├── .gitignore ├── .travis.yml ├── BermiInflector ├── BSD-LICENSE ├── Inflector.py ├── README.markdown ├── Rules │ ├── Base.py │ ├── English.py │ └── __init__.py ├── __init__.py └── tests.py ├── LICENSE ├── Makefile ├── README.markdown ├── doc ├── examples.xhtml ├── index.xhtml ├── lowlevel.xhtml ├── relationship_examples.xhtml ├── stylesheet.css └── template.tpl ├── setup.py ├── tox.ini └── twistar ├── __init__.py ├── dbconfig ├── __init__.py ├── base.py ├── mysql.py ├── postgres.py ├── pyodbc.py └── sqlite.py ├── dbobject.py ├── exceptions.py ├── registry.py ├── relationships.py ├── tests ├── __init__.py ├── mysql_config.py ├── postgres_config.py ├── sqlite_config.py ├── test_dbconfig.py ├── test_dbobject.py ├── test_relationships.py ├── test_transactions.py ├── test_utils.py └── utils.py ├── utils.py └── validation.py /.gitignore: -------------------------------------------------------------------------------- 1 | _trial_temp* 2 | apidoc/* 3 | doc/*.html 4 | *.pyc 5 | _trial_temp 6 | build 7 | dist 8 | twistar.egg-info 9 | .tox 10 | .coverage 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - env: TOXENV=py27-twisted12 7 | python: "2.7" 8 | - env: TOXENV=py27-twisted13 9 | python: "2.7" 10 | - env: TOXENV=py27-twisted14 11 | python: "2.7" 12 | - env: TOXENV=py27-twisted15 13 | python: "2.7" 14 | - env: TOXENV=py27-twisted16 15 | python: "2.7" 16 | - env: TOXENV=py35-twisted16 17 | python: "3.5" 18 | - env: TOXENV=pypy-twisted12 19 | python: "pypy" 20 | - env: TOXENV=pypy-twisted13 21 | python: "pypy" 22 | - env: TOXENV=pypy-twisted14 23 | python: "pypy" 24 | - env: TOXENV=pypy-twisted15 25 | python: "pypy" 26 | - env: TOXENV=pypy-twisted16 27 | python: "pypy" 28 | - env: TOXENV=pypy3-twisted16 29 | python: "pypy3" 30 | allow_failures: 31 | - env: TOXENV=pypy3-twisted16 32 | 33 | install: 34 | - pip install tox coveralls pep8 pyflakes 35 | 36 | script: make lint && tox 37 | 38 | after_success: coveralls 39 | -------------------------------------------------------------------------------- /BermiInflector/BSD-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 Bermi Ferrer Martinez 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software to deal in this software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of this software, and to permit 6 | persons to whom this software is furnished to do so, subject to the following 7 | condition: 8 | 9 | THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 11 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 12 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 13 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 14 | OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN 15 | THIS SOFTWARE. -------------------------------------------------------------------------------- /BermiInflector/Inflector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2006 Bermi Ferrer Martinez 4 | # 5 | # bermi a-t bermilabs - com 6 | # See the end of this file for the free software, open source license (BSD-style). 7 | 8 | from __future__ import absolute_import 9 | import re 10 | from .Rules.English import English 11 | 12 | class Inflector : 13 | """ 14 | Inflector for pluralizing and singularizing nouns. 15 | 16 | It provides methods for helping on creating programs 17 | based on naming conventions like on Ruby on Rails. 18 | """ 19 | 20 | def __init__( self, Inflector = English ) : 21 | assert callable(Inflector), "Inflector should be a callable obj" 22 | self.Inflector = Inflector() 23 | 24 | def pluralize(self, word) : 25 | '''Pluralizes nouns.''' 26 | return self.Inflector.pluralize(word) 27 | 28 | def singularize(self, word) : 29 | '''Singularizes nouns.''' 30 | return self.Inflector.singularize(word) 31 | 32 | def conditionalPlural(self, numer_of_records, word) : 33 | '''Returns the plural form of a word if first parameter is greater than 1''' 34 | return self.Inflector.conditionalPlural(numer_of_records, word) 35 | 36 | def titleize(self, word, uppercase = '') : 37 | '''Converts an underscored or CamelCase word into a sentence. 38 | The titleize function converts text like "WelcomePage", 39 | "welcome_page" or "welcome page" to this "Welcome Page". 40 | If the "uppercase" parameter is set to 'first' it will only 41 | capitalize the first character of the title.''' 42 | return self.Inflector.titleize(word, uppercase) 43 | 44 | def camelize(self, word): 45 | ''' Returns given word as CamelCased 46 | Converts a word like "send_email" to "SendEmail". It 47 | will remove non alphanumeric character from the word, so 48 | "who's online" will be converted to "WhoSOnline"''' 49 | return self.Inflector.camelize(word) 50 | 51 | def underscore(self, word) : 52 | ''' Converts a word "into_it_s_underscored_version" 53 | Convert any "CamelCased" or "ordinary Word" into an 54 | "underscored_word". 55 | This can be really useful for creating friendly URLs.''' 56 | return self.Inflector.underscore(word) 57 | 58 | def humanize(self, word, uppercase = '') : 59 | '''Returns a human-readable string from word 60 | Returns a human-readable string from word, by replacing 61 | underscores with a space, and by upper-casing the initial 62 | character by default. 63 | If you need to uppercase all the words you just have to 64 | pass 'all' as a second parameter.''' 65 | return self.Inflector.humanize(word, uppercase) 66 | 67 | 68 | def variablize(self, word) : 69 | '''Same as camelize but first char is lowercased 70 | Converts a word like "send_email" to "sendEmail". It 71 | will remove non alphanumeric character from the word, so 72 | "who's online" will be converted to "whoSOnline"''' 73 | return self.Inflector.variablize(word) 74 | 75 | def tableize(self, class_name) : 76 | ''' Converts a class name to its table name according to rails 77 | naming conventions. Example. Converts "Person" to "people" ''' 78 | return self.Inflector.tableize(class_name) 79 | 80 | def classify(self, table_name) : 81 | '''Converts a table name to its class name according to rails 82 | naming conventions. Example: Converts "people" to "Person" ''' 83 | return self.Inflector.classify(table_name) 84 | 85 | def ordinalize(self, number) : 86 | '''Converts number to its ordinal form. 87 | This method converts 13 to 13th, 2 to 2nd ...''' 88 | return self.Inflector.ordinalize(number) 89 | 90 | 91 | def unaccent(self, text) : 92 | '''Transforms a string to its unaccented version. 93 | This might be useful for generating "friendly" URLs''' 94 | return self.Inflector.unaccent(text) 95 | 96 | def urlize(self, text) : 97 | '''Transform a string its unaccented and underscored 98 | version ready to be inserted in friendly URLs''' 99 | return self.Inflector.urlize(text) 100 | 101 | 102 | def demodulize(self, module_name) : 103 | return self.Inflector.demodulize(module_name) 104 | 105 | def modulize(self, module_description) : 106 | return self.Inflector.modulize(module_description) 107 | 108 | def foreignKey(self, class_name, separate_class_name_and_id_with_underscore = 1) : 109 | ''' Returns class_name in underscored form, with "_id" tacked on at the end. 110 | This is for use in dealing with the database.''' 111 | return self.Inflector.foreignKey(class_name, separate_class_name_and_id_with_underscore) 112 | 113 | 114 | 115 | 116 | # Copyright (c) 2006 Bermi Ferrer Martinez 117 | # Permission is hereby granted, free of charge, to any person obtaining a copy 118 | # of this software to deal in this software without restriction, including 119 | # without limitation the rights to use, copy, modify, merge, publish, 120 | # distribute, sublicense, and/or sell copies of this software, and to permit 121 | # persons to whom this software is furnished to do so, subject to the following 122 | # condition: 123 | # 124 | # THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 125 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 126 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 127 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 128 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 129 | # OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN 130 | # THIS SOFTWARE. 131 | -------------------------------------------------------------------------------- /BermiInflector/README.markdown: -------------------------------------------------------------------------------- 1 | # Inflector for Python 2 | 3 | The Inflector is used for getting the plural and singular form of nouns. This piece of code helps on creating code that favors convention over configuration. 4 | 5 | Only English and Spanish nouns are supported. The English version is a port of Ruby on Rails Inflector, while the Spanish Version has been developed from scratch with the help of Carles Sadurní. 6 | 7 | Apart from converting singulars and plurals, this module also handles necessary string conversion for convention based applications like: 8 | 9 | Available methods are: 10 | 11 | ## pluralize(word) 12 | 13 | Pluralizes nouns. 14 | 15 | ## singularize(word) 16 | 17 | Singularizes nouns. 18 | 19 | ## conditionalPlural(numer_of_records, word) 20 | 21 | Returns the plural form of a word if first parameter is greater than 1 22 | 23 | ## titleize(word, uppercase = '') 24 | 25 | Converts an underscored or CamelCase word into a sentence. 26 | The titleize function converts text like "WelcomePage", 27 | "welcome_page" or "welcome page" to this "Welcome Page". 28 | If the "uppercase" parameter is set to 'first' it will only 29 | capitalize the first character of the title. 30 | 31 | ## camelize(word): 32 | 33 | Returns given word as CamelCased 34 | Converts a word like "send_email" to "SendEmail". It 35 | will remove non alphanumeric character from the word, so 36 | "who's online" will be converted to "WhoSOnline" 37 | 38 | ## underscore(word) 39 | 40 | Converts a word "into_it_s_underscored_version" 41 | Convert any "CamelCased" or "ordinary Word" into an 42 | "underscored_word". 43 | This can be really useful for creating friendly URLs. 44 | 45 | ## humanize(word, uppercase = '') 46 | 47 | Returns a human-readable string from word 48 | Returns a human-readable string from word, by replacing 49 | underscores with a space, and by upper-casing the initial 50 | character by default. 51 | If you need to uppercase all the words you just have to 52 | pass 'all' as a second parameter. 53 | 54 | 55 | ## variablize(word) 56 | 57 | Same as camelize but first char is lowercased 58 | Converts a word like "send_email" to "sendEmail". It 59 | will remove non alphanumeric character from the word, so 60 | "who's online" will be converted to "whoSOnline" 61 | return self.Inflector.variablize(word) 62 | 63 | ## tableize(class_name) 64 | 65 | Converts a class name to its table name according to rails 66 | naming conventions. Example. Converts "Person" to "people" 67 | 68 | ## classify(table_name) 69 | 70 | Converts a table name to its class name according to rails 71 | naming conventions. Example: Converts "people" to "Person" 72 | 73 | ## ordinalize(number) 74 | Converts number to its ordinal form. 75 | This method converts 13 to 13th, 2 to 2nd ... 76 | 77 | ## unaccent(text) 78 | 79 | Transforms a string to its unaccented version. 80 | This might be useful for generating "friendly" URLs 81 | 82 | ## urlize(text) 83 | 84 | Transform a string its unaccented and underscored 85 | version ready to be inserted in friendly URLs 86 | 87 | ## foreignKey(class_name, separate_class_name_and_id_with_underscore = 1) 88 | 89 | Returns class_name in underscored form, with "_id" tacked on at the end. 90 | This is for use in dealing with the database. 91 | -------------------------------------------------------------------------------- /BermiInflector/Rules/Base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2006 Bermi Ferrer Martinez 4 | # bermi a-t bermilabs - com 5 | # See the end of this file for the free software, open source license (BSD-style). 6 | 7 | from __future__ import absolute_import 8 | import re 9 | from six.moves import range 10 | 11 | class Base: 12 | '''Locale inflectors must inherit from this base class inorder to provide 13 | the basic Inflector functionality''' 14 | 15 | def conditionalPlural(self, numer_of_records, word) : 16 | '''Returns the plural form of a word if first parameter is greater than 1''' 17 | 18 | if numer_of_records > 1 : 19 | return self.pluralize(word) 20 | else : 21 | return word 22 | 23 | 24 | def titleize(self, word, uppercase = '') : 25 | '''Converts an underscored or CamelCase word into a English sentence. 26 | The titleize function converts text like "WelcomePage", 27 | "welcome_page" or "welcome page" to this "Welcome Page". 28 | If second parameter is set to 'first' it will only 29 | capitalize the first character of the title.''' 30 | 31 | if(uppercase == 'first'): 32 | return self.humanize(self.underscore(word)).capitalize() 33 | else : 34 | return self.humanize(self.underscore(word)).title() 35 | 36 | 37 | def camelize(self, word): 38 | ''' Returns given word as CamelCased 39 | Converts a word like "send_email" to "SendEmail". It 40 | will remove non alphanumeric character from the word, so 41 | "who's online" will be converted to "WhoSOnline"''' 42 | return ''.join(w[0].upper() + w[1:] for w in re.sub('[^A-Z^a-z^0-9^:]+', ' ', word).split(' ')) 43 | 44 | def underscore(self, word) : 45 | ''' Converts a word "into_it_s_underscored_version" 46 | Convert any "CamelCased" or "ordinary Word" into an 47 | "underscored_word". 48 | This can be really useful for creating friendly URLs.''' 49 | 50 | return re.sub('[^A-Z^a-z^0-9^\/]+','_', \ 51 | re.sub('([a-z\d])([A-Z])','\\1_\\2', \ 52 | re.sub('([A-Z]+)([A-Z][a-z])','\\1_\\2', re.sub('::', '/',word)))).lower() 53 | 54 | 55 | def humanize(self, word, uppercase = '') : 56 | '''Returns a human-readable string from word 57 | Returns a human-readable string from word, by replacing 58 | underscores with a space, and by upper-casing the initial 59 | character by default. 60 | If you need to uppercase all the words you just have to 61 | pass 'all' as a second parameter.''' 62 | 63 | if(uppercase == 'first'): 64 | return re.sub('_id$', '', word).replace('_',' ').capitalize() 65 | else : 66 | return re.sub('_id$', '', word).replace('_',' ').title() 67 | 68 | 69 | def variablize(self, word) : 70 | '''Same as camelize but first char is lowercased 71 | Converts a word like "send_email" to "sendEmail". It 72 | will remove non alphanumeric character from the word, so 73 | "who's online" will be converted to "whoSOnline"''' 74 | word = self.camelize(word) 75 | return word[0].lower()+word[1:] 76 | 77 | def tableize(self, class_name) : 78 | ''' Converts a class name to its table name according to rails 79 | naming conventions. Example. Converts "Person" to "people" ''' 80 | return self.pluralize(self.underscore(class_name)) 81 | 82 | 83 | def classify(self, table_name) : 84 | '''Converts a table name to its class name according to rails 85 | naming conventions. Example: Converts "people" to "Person" ''' 86 | return self.camelize(self.singularize(table_name)) 87 | 88 | 89 | def ordinalize(self, number) : 90 | '''Converts number to its ordinal English form. 91 | This method converts 13 to 13th, 2 to 2nd ...''' 92 | tail = 'th' 93 | if number % 100 == 11 or number % 100 == 12 or number % 100 == 13: 94 | tail = 'th' 95 | elif number % 10 == 1 : 96 | tail = 'st' 97 | elif number % 10 == 2 : 98 | tail = 'nd' 99 | elif number % 10 == 3 : 100 | tail = 'rd' 101 | 102 | return str(number)+tail 103 | 104 | 105 | def unaccent(self, text) : 106 | '''Transforms a string to its unaccented version. 107 | This might be useful for generating "friendly" URLs''' 108 | find = u'\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF' 109 | replace = u'AAAAAAACEEEEIIIIDNOOOOOOUUUUYTsaaaaaaaceeeeiiiienoooooouuuuyty' 110 | return self.string_replace(text, find, replace) 111 | 112 | def string_replace (self, word, find, replace) : 113 | '''This function returns a copy of word, translating 114 | all occurrences of each character in find to the 115 | corresponding character in replace''' 116 | for k in range(0,len(find)) : 117 | word = re.sub(find[k], replace[k], word) 118 | 119 | return word 120 | 121 | def urlize(self, text) : 122 | '''Transform a string its unaccented and underscored 123 | version ready to be inserted in friendly URLs''' 124 | return re.sub('^_|_$','',self.underscore(self.unaccent(text))) 125 | 126 | 127 | def demodulize(self, module_name) : 128 | return self.humanize(self.underscore(re.sub('^.*::','',module_name))) 129 | 130 | def modulize(self, module_description) : 131 | return self.camelize(self.singularize(module_description)) 132 | 133 | def foreignKey(self, class_name, separate_class_name_and_id_with_underscore = 1) : 134 | ''' Returns class_name in underscored form, with "_id" tacked on at the end. 135 | This is for use in dealing with the database.''' 136 | if separate_class_name_and_id_with_underscore : 137 | tail = '_id' 138 | else : 139 | tail = 'id' 140 | return self.underscore(self.demodulize(class_name))+tail; 141 | 142 | 143 | 144 | # Copyright (c) 2006 Bermi Ferrer Martinez 145 | # Permission is hereby granted, free of charge, to any person obtaining a copy 146 | # of this software to deal in this software without restriction, including 147 | # without limitation the rights to use, copy, modify, merge, publish, 148 | # distribute, sublicense, and/or sell copies of this software, and to permit 149 | # persons to whom this software is furnished to do so, subject to the following 150 | # condition: 151 | # 152 | # THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 153 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 154 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 155 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 156 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 157 | # OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN 158 | # THIS SOFTWARE. -------------------------------------------------------------------------------- /BermiInflector/Rules/English.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2006 Bermi Ferrer Martinez 4 | # bermi a-t bermilabs - com 5 | # 6 | # See the end of this file for the free software, open source license (BSD-style). 7 | 8 | from __future__ import absolute_import 9 | import re 10 | from .Base import Base 11 | from six.moves import range 12 | 13 | class English (Base): 14 | """ 15 | Inflector for pluralize and singularize English nouns. 16 | 17 | This is the default Inflector for the Inflector obj 18 | """ 19 | 20 | def pluralize(self, word) : 21 | '''Pluralizes English nouns.''' 22 | 23 | rules = [ 24 | ['(?i)(quiz)$' , '\\1zes'], 25 | ['^(?i)(ox)$' , '\\1en'], 26 | ['(?i)([m|l])ouse$' , '\\1ice'], 27 | ['(?i)(matr|vert|ind)ix|ex$' , '\\1ices'], 28 | ['(?i)(x|ch|ss|sh)$' , '\\1es'], 29 | ['(?i)([^aeiouy]|qu)ies$' , '\\1y'], 30 | ['(?i)([^aeiouy]|qu)y$' , '\\1ies'], 31 | ['(?i)(hive)$' , '\\1s'], 32 | ['(?i)(?:([^f])fe|([lr])f)$' , '\\1\\2ves'], 33 | ['(?i)sis$' , 'ses'], 34 | ['(?i)([ti])um$' , '\\1a'], 35 | ['(?i)(buffal|tomat)o$' , '\\1oes'], 36 | ['(?i)(bu)s$' , '\\1ses'], 37 | ['(?i)(alias|status)' , '\\1es'], 38 | ['(?i)(octop|vir)us$' , '\\1i'], 39 | ['(?i)(ax|test)is$' , '\\1es'], 40 | ['(?i)s$' , 's'], 41 | ['(?i)$' , 's'] 42 | ] 43 | 44 | uncountable_words = ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep'] 45 | 46 | irregular_words = { 47 | 'person' : 'people', 48 | 'man' : 'men', 49 | 'child' : 'children', 50 | 'sex' : 'sexes', 51 | 'move' : 'moves' 52 | } 53 | 54 | lower_cased_word = word.lower(); 55 | 56 | for uncountable_word in uncountable_words: 57 | if lower_cased_word[-1*len(uncountable_word):] == uncountable_word : 58 | return word 59 | 60 | for irregular in irregular_words.keys(): 61 | match = re.search('('+irregular+')$',word, re.IGNORECASE) 62 | if match: 63 | return re.sub('(?i)'+irregular+'$', match.expand('\\1')[0]+irregular_words[irregular][1:], word) 64 | 65 | for rule in range(len(rules)): 66 | match = re.search(rules[rule][0], word, re.IGNORECASE) 67 | if match : 68 | groups = match.groups() 69 | for k in range(0,len(groups)) : 70 | if groups[k] == None : 71 | rules[rule][1] = rules[rule][1].replace('\\'+str(k+1), '') 72 | 73 | return re.sub(rules[rule][0], rules[rule][1], word) 74 | 75 | return word 76 | 77 | 78 | def singularize (self, word) : 79 | '''Singularizes English nouns.''' 80 | 81 | rules = [ 82 | ['(?i)(quiz)zes$' , '\\1'], 83 | ['(?i)(matr)ices$' , '\\1ix'], 84 | ['(?i)(vert|ind)ices$' , '\\1ex'], 85 | ['(?i)^(ox)en' , '\\1'], 86 | ['(?i)(alias|status)es$' , '\\1'], 87 | ['(?i)([octop|vir])i$' , '\\1us'], 88 | ['(?i)(cris|ax|test)es$' , '\\1is'], 89 | ['(?i)(shoe)s$' , '\\1'], 90 | ['(?i)(o)es$' , '\\1'], 91 | ['(?i)(bus)es$' , '\\1'], 92 | ['(?i)([m|l])ice$' , '\\1ouse'], 93 | ['(?i)(x|ch|ss|sh)es$' , '\\1'], 94 | ['(?i)(m)ovies$' , '\\1ovie'], 95 | ['(?i)(s)eries$' , '\\1eries'], 96 | ['(?i)([^aeiouy]|qu)ies$' , '\\1y'], 97 | ['(?i)([lr])ves$' , '\\1f'], 98 | ['(?i)(tive)s$' , '\\1'], 99 | ['(?i)(hive)s$' , '\\1'], 100 | ['(?i)([^f])ves$' , '\\1fe'], 101 | ['(?i)(^analy)ses$' , '\\1sis'], 102 | ['(?i)((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$' , '\\1\\2sis'], 103 | ['(?i)([ti])a$' , '\\1um'], 104 | ['(?i)(n)ews$' , '\\1ews'], 105 | ['(?i)s$' , ''], 106 | ]; 107 | 108 | uncountable_words = ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep','sms']; 109 | 110 | irregular_words = { 111 | 'people' : 'person', 112 | 'men' : 'man', 113 | 'children' : 'child', 114 | 'sexes' : 'sex', 115 | 'moves' : 'move' 116 | } 117 | 118 | lower_cased_word = word.lower(); 119 | 120 | for uncountable_word in uncountable_words: 121 | if lower_cased_word[-1*len(uncountable_word):] == uncountable_word : 122 | return word 123 | 124 | for irregular in irregular_words.keys(): 125 | match = re.search('('+irregular+')$',word, re.IGNORECASE) 126 | if match: 127 | return re.sub('(?i)'+irregular+'$', match.expand('\\1')[0]+irregular_words[irregular][1:], word) 128 | 129 | 130 | for rule in range(len(rules)): 131 | match = re.search(rules[rule][0], word, re.IGNORECASE) 132 | if match : 133 | groups = match.groups() 134 | for k in range(0,len(groups)) : 135 | if groups[k] == None : 136 | rules[rule][1] = rules[rule][1].replace('\\'+str(k+1), '') 137 | 138 | return re.sub(rules[rule][0], rules[rule][1], word) 139 | 140 | return word 141 | 142 | 143 | 144 | # Copyright (c) 2006 Bermi Ferrer Martinez 145 | # Permission is hereby granted, free of charge, to any person obtaining a copy 146 | # of this software to deal in this software without restriction, including 147 | # without limitation the rights to use, copy, modify, merge, publish, 148 | # distribute, sublicense, and/or sell copies of this software, and to permit 149 | # persons to whom this software is furnished to do so, subject to the following 150 | # condition: 151 | # 152 | # THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 153 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 154 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 155 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 156 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 157 | # OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN 158 | # THIS SOFTWARE. -------------------------------------------------------------------------------- /BermiInflector/Rules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmuller/twistar/1eb46ff2577473e0a26932ee57473e26203a3db2/BermiInflector/Rules/__init__.py -------------------------------------------------------------------------------- /BermiInflector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmuller/twistar/1eb46ff2577473e0a26932ee57473e26203a3db2/BermiInflector/__init__.py -------------------------------------------------------------------------------- /BermiInflector/tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2006 Bermi Ferrer Martinez 5 | # 6 | # bermi a-t bermilabs - com 7 | # 8 | from __future__ import absolute_import 9 | import unittest 10 | from .Inflector import Inflector, English, Spanish 11 | 12 | class EnglishInflectorTestCase(unittest.TestCase): 13 | singular_to_plural = { 14 | "search" : "searches", 15 | "switch" : "switches", 16 | "fix" : "fixes", 17 | "box" : "boxes", 18 | "process" : "processes", 19 | "address" : "addresses", 20 | "case" : "cases", 21 | "stack" : "stacks", 22 | "wish" : "wishes", 23 | "fish" : "fish", 24 | 25 | "category" : "categories", 26 | "query" : "queries", 27 | "ability" : "abilities", 28 | "agency" : "agencies", 29 | "movie" : "movies", 30 | 31 | "archive" : "archives", 32 | 33 | "index" : "indices", 34 | 35 | "wife" : "wives", 36 | "safe" : "saves", 37 | "half" : "halves", 38 | 39 | "move" : "moves", 40 | 41 | "salesperson" : "salespeople", 42 | "person" : "people", 43 | 44 | "spokesman" : "spokesmen", 45 | "man" : "men", 46 | "woman" : "women", 47 | 48 | "basis" : "bases", 49 | "diagnosis" : "diagnoses", 50 | 51 | "datum" : "data", 52 | "medium" : "media", 53 | "analysis" : "analyses", 54 | 55 | "node_child" : "node_children", 56 | "child" : "children", 57 | 58 | "experience" : "experiences", 59 | "day" : "days", 60 | 61 | "comment" : "comments", 62 | "foobar" : "foobars", 63 | "newsletter" : "newsletters", 64 | 65 | "old_news" : "old_news", 66 | "news" : "news", 67 | 68 | "series" : "series", 69 | "species" : "species", 70 | 71 | "quiz" : "quizzes", 72 | 73 | "perspective" : "perspectives", 74 | 75 | "ox" : "oxen", 76 | "photo" : "photos", 77 | "buffalo" : "buffaloes", 78 | "tomato" : "tomatoes", 79 | "dwarf" : "dwarves", 80 | "elf" : "elves", 81 | "information" : "information", 82 | "equipment" : "equipment", 83 | "bus" : "buses", 84 | "status" : "statuses", 85 | "mouse" : "mice", 86 | 87 | "louse" : "lice", 88 | "house" : "houses", 89 | "octopus" : "octopi", 90 | "virus" : "viri", 91 | "alias" : "aliases", 92 | "portfolio" : "portfolios", 93 | 94 | "vertex" : "vertices", 95 | "matrix" : "matrices", 96 | 97 | "axis" : "axes", 98 | "testis" : "testes", 99 | "crisis" : "crises", 100 | 101 | "rice" : "rice", 102 | "shoe" : "shoes", 103 | 104 | "horse" : "horses", 105 | "prize" : "prizes", 106 | "edge" : "edges" 107 | } 108 | def setUp(self): 109 | self.inflector = Inflector(English) 110 | 111 | def tearDown(self): 112 | self.inflector = None 113 | 114 | def test_pluralize(self) : 115 | for singular in self.singular_to_plural.keys() : 116 | assert self.inflector.pluralize(singular) == self.singular_to_plural[singular], \ 117 | 'English Inlector pluralize(%s) should produce "%s" and NOT "%s"' % (singular, self.singular_to_plural[singular], self.inflector.pluralize(singular)) 118 | 119 | def test_singularize(self) : 120 | for singular in self.singular_to_plural.keys() : 121 | assert self.inflector.singularize(self.singular_to_plural[singular]) == singular, \ 122 | 'English Inlector singularize(%s) should produce "%s" and NOT "%s"' % (self.singular_to_plural[singular], singular, self.inflector.singularize(self.singular_to_plural[singular])) 123 | 124 | 125 | 126 | InflectorTestSuite = unittest.TestSuite() 127 | InflectorTestSuite.addTest(EnglishInflectorTestCase("test_pluralize")) 128 | InflectorTestSuite.addTest(EnglishInflectorTestCase("test_singularize")) 129 | runner = unittest.TextTestRunner() 130 | runner.run(InflectorTestSuite) 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The files in the BermiInflector directory are released under the 2 | license described in the file BSD-LICENSE found in that folder. 3 | All other code is released with the following copyright and under 4 | the following license: 5 | 6 | Copyright (c) 2010 Butterfat, LLC 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | "Software"), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYDOCTOR=pydoctor 2 | 3 | docs: 4 | $(PYDOCTOR) --make-html --html-output apidoc --add-package twistar --project-name=twistar --project-url=http://findingscience.com/twistar --html-use-sorttable --html-use-splitlinks --html-shorten-lists 5 | lore --config template=doc/template.tpl doc/*.xhtml 6 | 7 | test: 8 | trial twistar 9 | 10 | install: 11 | python setup.py install 12 | 13 | lint: 14 | pep8 --ignore=E303 --max-line-length=140 ./twistar 15 | find ./twistar -name '*.py' | xargs pyflakes 16 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # twistar: Asynchronous Python ORM 2 | [![Build Status](https://secure.travis-ci.org/bmuller/twistar.png?branch=master)](https://travis-ci.org/bmuller/twistar) [![Coverage Status](https://coveralls.io/repos/bmuller/twistar/badge.svg?branch=master&service=github)](https://coveralls.io/github/bmuller/twistar?branch=master) 3 | 4 | The Twistar Project provides an ActiveRecord (ORM) pattern interface to the Twisted Project's RDBMS library. This file contains minimal documentation - see the project home page at http://findingscience.com/twistar for more information. 5 | 6 | ## Installation 7 | 8 | ``` 9 | pip install twistar 10 | ``` 11 | 12 | ## Usage 13 | Your database must be one of: MySQL, PostgreSQL, or SQLite. The only DBAPI modules supported by Twistar are: MySQLdb, psycopg2, and sqlite3 - at least one of these must be installed. 14 | 15 | Here's the obligatory TL;DR example of creating a User record, assuming that there is a table named "users" with varchar columns for first_name and last_name and an int age column: 16 | 17 | ```python 18 | #!/usr/bin/env python 19 | from twisted.enterprise import adbapi 20 | from twistar.registry import Registry 21 | from twistar.dbobject import DBObject 22 | from twisted.internet import reactor 23 | 24 | class User(DBObject): 25 | pass 26 | 27 | def done(user): 28 | print "A user was just created with the name %s" % user.first_name 29 | reactor.stop() 30 | 31 | # Connect to the DB 32 | Registry.DBPOOL = adbapi.ConnectionPool('MySQLdb', user="twistar", passwd="apass", db="twistar") 33 | 34 | # make a user 35 | u = User() 36 | u.first_name = "John" 37 | u.last_name = "Smith" 38 | u.age = 25 39 | 40 | # Or, use this shorter version: 41 | u = User(first_name="John", last_name="Smith", age=25) 42 | 43 | # save the user 44 | u.save().addCallback(done) 45 | 46 | reactor.run() 47 | ``` 48 | 49 | Then, finding this user is easy: 50 | 51 | ```python 52 | def found(users): 53 | print "I found %i users!" % len(users) 54 | for user in users: 55 | print "User: %s %s" % (user.first_name, user.last_name) 56 | 57 | u = User.findBy(first_name="John", age=25).addCallback(found) 58 | ``` 59 | 60 | This is a very simple example - see http://findingscience.com/twistar for more complicated examples and additional uses. 61 | 62 | ## Testing 63 | To run unit-tests you simply require [Tox](https://tox.readthedocs.org) 64 | 65 | To run the tests: 66 | ``` 67 | tox 68 | ``` 69 | 70 | By default, the tests are run with the database driver sqlite3. To change this, set the DBTYPE environment variable: 71 | 72 | ``` 73 | DBTYPE=postgres trial twistar 74 | DBTYPE=mysql trial twistar 75 | ``` 76 | 77 | You'll need a database named "twistar" for each of those tests (or you can change the dbname, user, etc in the `_config.py` file in the tests folder). 78 | 79 | ## Documentation 80 | If you intent on generating API documentation, you will need pydoctor. If you want to generate the user documentation, you will need to install Twisted Lore. 81 | 82 | To generate documentation: 83 | 84 | ``` 85 | make docs 86 | ``` 87 | 88 | Then open the docs/index.html file in a browser. 89 | -------------------------------------------------------------------------------- /doc/examples.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Examples 7 | 8 | 9 | 10 |

Simple Examples

11 |

12 | Since Twistar uses Twisted, most methods and function calls will return a twisted.internet.defer.Deferred 13 | object. You must understand the proper use of these objects before most of this documentation will make sense. 14 |

15 | 16 |

Initialization

17 |

Before using Twistar a connection to the DB must be made. To connect to the database 18 | use Twisted's twisted.enterprise.adapi module and the ConnectionPool 19 | object. The Registry class is used to keep track of that pool. For instance, this is the 20 | method to specify a connection to a MySQL database: 21 |

22 |
 23 | from twisted.enterprise import adbapi
 24 | from twistar.registry import Registry
 25 | 
 26 | Registry.DBPOOL = adbapi.ConnectionPool('MySQLdb', user="twistar", passwd="apass", db="twistar")
 27 | 
28 |

29 | The modules implementing DBAPI that are supported by Twistar are: 30 |

35 |

36 | 37 |

Database Creation

38 |

Twistar does not provide DB creation / migration functionality beyond asynchronously making SQL queries. 39 | There are plenty of utilities in existance to provide these capabilities. If you need to create tables 40 | in an asynchronous manner, you can always execute the creation SQL directly. 41 |

42 |

43 | Objects will assume that the plural version of the object's class name will be the table name. For instance, 44 | if the class name is Person, then the tablename should be People. If it is Chicken, the tablename should be 45 | Chickens. If you want to manually specify a tablename, you can do that with the 46 | TABLENAME class variable (see the 47 | DBObject definition). 48 |

49 |

50 | Column names should have no spaces (with words separated by a _) and should generally be lower case. 51 | All object tables (that don't describe relationships) should have 52 | an auto-incrementing integer column named id. 53 |

54 | 55 |

Defining and Interacting With Objects

56 |

57 | Objects representing rows in a database need only extend the 58 | DBObject class. 59 |

60 |
 61 | from twistar.dbobject import DBObject
 62 | 
 63 | class User(DBObject):
 64 |      pass
 65 | 
66 |

67 | Assuming that the users table has some VARCHAR fields like first_name and last_name and, say, 68 | and integer field named age (along with the required auto-incrementing integer column named 69 | id), then object properties can be assigned: 70 |

71 | 72 |
 73 | def done(user):
 74 |      print "A user has just been saved with id: %i" % user.id
 75 | 
 76 | u = User()
 77 | u.first_name = "John"
 78 | u.last_name = "Smith"
 79 | u.age = 25
 80 | u.save().addCallback(done)
 81 | 
 82 | # The following is equivalent
 83 | u = User(first_name="John", last_name="Smith", age=25)
 84 | u.save().addCallback(done)
 85 | 
86 | 87 |

88 | Any additional properties that are set that don't correspond to column names are ignored on the save. For instance, 89 | had a middle_name property been set for the user it would have simply been ignored when the object was saved. 90 |

91 | 92 |

93 | Methods you would traditionally find in an active record style RDBMS are also available, for instance to 94 | test for existance and delete an object instance. 95 |

96 | 97 |

Finding Objects

98 |

To find an object, use the class's find method. It accepts a number of 99 | arguments to group, sort, and limit results. The simplest way to use the method is to get an object 100 | instance by id. For instance, if we wanted to find the user with an id of 1: 101 |

102 |
103 | User.find(1).addCallback(...)
104 | 
105 |

106 | Additional constriants can be specified using the where 107 | paramter and others: 108 |

109 |
110 | User.find(where=['first_name = ? AND last_name = ?', "John", "Smith"], limit=1).addCallback(...)
111 | 
112 |

There are many more options, all of which are described on the 113 | DBObject API reference page. 114 |

115 |

116 | One thing to note: if either limit is set to 1 or the query 117 | is by id, then the resulting deferred will return either the 118 | found object or None if not found. Otherwise, an array is returned 119 | containing the found objects. 120 |

121 | 122 |

Class Methods

123 |

124 | Additional class methods (other than find) includes ones for getting an array of all objects 125 | (using the all() method), deleting all instances 126 | (using the deleteAll() method), getting a count of all instances 127 | (using the count() method), and determining whether or not 128 | any particular objects exist (using the exists() method). For instance: 129 |

130 |
131 | def printAll(users):
132 |      for user in users:
133 |           print "First: %s Last: %s" % (user.first_name, user.last_name)
134 | 
135 | User.all().addCallback(printAll)
136 | 
137 | 
138 | def printExists(doesExist):
139 |      if doesExist:
140 |           print "User exists!"
141 |      else:
142 |           print "User does not exist."
143 | 
144 | User.exists(['first_name = ?', "John"]).addCallback(printExists)
145 | 
146 | 147 | 148 |

Validations

149 |

150 | Twistar also supports validations. Validations describe constraints on an object's 151 | parameters. If those constraints are violated, then an object will not be saved (this 152 | includes both creating and updating). In such a case a special parameter in the object 153 | keeps track of the errors and the messages. 154 |

155 |

156 | As an example, let's say that we want all users to have a first name set and a last 157 | name that has a length between 1 and 256 characters. First, we describe the class 158 | User as above and then add restrictions on it. 159 |

160 |
161 | class User(DBObject):
162 |      pass
163 | User.validatesPresenceOf('first_name')
164 | User.validatesLengthOf('last_name', range=xrange(1,257))
165 | 
166 |

167 | If we look at an object without those parameters, it will be invalid: 168 |

169 |
170 | def vcheck(isValid, object):
171 |      if not isValid:
172 |           print "Object not valid: %s" % str(u.errors)
173 | u = User()
174 | u.isValid().addCallback(vcheck, u)
175 | 
176 |

177 | If we try to save an invalid object, nothing happens: 178 |

179 |
180 | def checkUser(user):
181 |      print user.id # this will be None
182 |      print user.errors.errorsFor('first_name')
183 |      print user.errors.errorsFor('last_name')
184 |      print "There were %d errors" % len(user.errors)
185 | User().save().addCallback(checkUser)
186 | 
187 |

188 | You can also create your own validity function. It should accept an object as 189 | a parameter and then add errors as necessary. It can return a Deferred 190 | if necessary; the return value from the function will be ignored. 191 |

192 |
193 | def myValidator(user):
194 |      if user.first_name != "fred":
195 |           user.errors.add('first_name', "must be Fred!")
196 | User.addValidator(myValidator)
197 | 
198 | def test(user):
199 |      # This will print "First Name must be Fred!"
200 |      print user.errors 
201 |      print user.id # None
202 | User(first_name='not fred').save().addCallback(test)
203 | 
204 |

205 | For more information see the Validator and 206 | Errors classes. 207 |

208 | 209 |

DBAPI Column Objects

210 |

211 | The DBAPI specification requires that 212 | implementing modules define classes corresponding to certain column types (like 213 | the class Date). Twistar provides a driver agnostic method for 214 | using those classes using the Registry class: 215 |

216 |
217 | Date = Registry.getDBAPIClass("Date")
218 | bob = User(first_name="Bob", dob=Date(1970, 1, 1))
219 | 
220 |

221 | Then, regardless of which DBAPI module you are using, your code will always be using the correct 222 | Date class. 223 |

224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /doc/index.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Twistar Documentation Index 8 | 9 | 10 | 11 | 12 |

Twistar Documentation Index

13 |

14 | Twistar is a Python implementation of the active record pattern 15 | (also known as an object-relational mapper or ORM) that uses the Twisted framework's 16 | RDBMS support to provide a non-blocking interface to 17 | relational databases. 18 |

19 |

20 | Since Twistar uses Twisted, most method and function calls will return a twisted.internet.defer.Deferred 21 | object. You must understand the propper use of these objects before most of this documentation will make sense. 22 |

23 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /doc/lowlevel.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Low Level Examples 7 | 8 | 9 | 10 |

Low Level Examples

11 | 12 |

SQL Execution

13 |

After database initialization and assignment to the Registry.DBPOOL 14 | variable, you can always utilize the twisted.enterprise.adapi.ConnectionPool's 15 | API directly: 16 |

17 |
18 | from twisted.enterprise import adbapi
19 | from twistar.registry import Registry
20 | 
21 | def done(result):
22 |      print "I just made a table"
23 | 
24 | Registry.DBPOOL = adbapi.ConnectionPool('MySQLdb', user="twistar", passwd="apass", db="twistar")
25 | Registry.DBPOOL.runQuery("CREATE DATABASE users (id INT ...").addCallback(done)
26 | 
27 |

The alternative, described below, might be much more useful for basic CRUD operations.

28 | 29 |

CRUD Interface

30 |

31 | In the InteractionBase you'll find all of the create/read/update/delete (CRUD) 32 | methods supported by the internal configuration class. To use these methods, use the Registry to get the current configuration 33 | object: 34 |

35 |
36 | from twisted.enterprise import adbapi
37 | from twistar.registry import Registry
38 | Registry.DBPOOL = adbapi.ConnectionPool('MySQLdb', user="twistar", passwd="apass", db="twistar")
39 | 
40 | dbconfig = Registry.getConfig()
41 | d = dbconfig.select("users", where=['first_name = ?', 'Bob'], orderby='last_name DESC', limit=100)
42 | d.addCallback(someResultHandlerFunction)
43 | 
44 |

45 | Each of the CRUD methods gives you more control on the query that is generated. For more information, see the following 46 | methods in InteractionBase: 47 |

54 |

55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /doc/relationship_examples.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Relationship Examples 7 | 8 | 9 | 10 |

Relationship Examples

11 |

Twistar is an Object Relational Mapper (ORM). 12 | It therefore defines ways of interacting with objects and other objects that have relationships with them. The code 13 | here is not necessarily great Twisted code; where typically you would expect to see DeferredLists and 14 | inlineCallbacks there are none to provide clarity to those new to Twisted. 15 |

16 |

17 | More information on relationships can be found in the Relationship class. 18 |

19 |

Has One

20 |

Perhaps the simplest relationship is the Has One relationship. In this example, we will 21 | be using a User object that Has One Avatar 22 | object. 23 |

24 |
 25 | from twistar.dbobject import DBObject
 26 | from twistar.registry import Registry
 27 | 
 28 | class User(DBObject):
 29 |      HASONE = ['avatar']
 30 | 
 31 | class Avatar(DBObject):
 32 |      pass
 33 | 
 34 | Registry.register(User, Avatar)
 35 | 
36 |

37 | In this example, we specify that each user can have one avatar. The database at this point should have two tables: 38 |

    39 |
  1. A users table with at least a column id.
  2. 40 |
  3. A avatars table with at least two columns: id and user_id.
  4. 41 |
42 | It is also necessary to register our classes with the registry before we can start utilizing relationships (this is 43 | so the module can find and instantiate them when necessary). 44 |

45 |

46 | For the HASONE class variable we can optionally give a dictionary that provides additional information: 47 |

48 |
 49 | from twistar.dbobject import DBObject
 50 | from twistar.registry import Registry
 51 | 
 52 | class User(DBObject):
 53 |      HASONE = [{'name': 'avatar', 'class_name': 'Avatar', foreign_key: 'user_id'}]
 54 | 
 55 | class Avatar(DBObject):
 56 |      pass
 57 | 
 58 | Registry.register(User, Avatar)
 59 | 
60 |

61 | There are additional options as well: see the Relationships class 62 | for more information. 63 |

64 |

65 | At this point we can use the relationship and assign user's avatar: 66 |

67 |
 68 | def onPicSave(picture, user):
 69 |      user.picture.set(picture)
 70 | 
 71 | def onUserSave(user):
 72 |      Avatar(file="somewhere").save().addCallback(onPicSave, user)
 73 | 
 74 | User(first_name="Bob").save().addCallback(onUserSave)
 75 | 
76 |

We can then get it:

77 |
 78 | def foundPicture(picture):
 79 |      print picture.file
 80 | 
 81 | def foundUser(user):
 82 |      user.picture.get().addCallback(foundPicture)
 83 | 
 84 | User.find(where=['first_name = ?', "Bob"], limit=1).addCallback(foundUser)
 85 | 
86 |

Has Many

87 |

Another relationship is used when one object "has many" of another. For instance, a User 88 | may have many Pictures. 89 |

90 |
 91 | from twistar.dbobject import DBObject
 92 | from twistar.registry import Registry
 93 | 
 94 | class User(DBObject):
 95 |      HASMANY = ['pictures']
 96 | 
 97 | class Picture(DBObject):
 98 |      pass
 99 | 
100 | Registry.register(User, Picture)
101 | 
102 |

Again, the list assigned to HASMANY can have dictionaries in it just like HASONE that 103 | contain additional configuration options. For this relationship, the database should at this point should have two tables: 104 |

    105 |
  1. A users table with at least a column id.
  2. 106 |
  3. A pictures table with at least two columns: id and user_id.
  4. 107 |
108 | As in the previous example, we can get and set a user's pictures using a special property of user: 109 | pictures. 110 |

111 |
112 | from twisted.internet.defer import inlineCallbacks
113 | 
114 | @inlineCallbacks
115 | def addPictures():
116 |      # set some pics
117 |      user = yield User(first_name="Bob").save()
118 |      picone = yield Picture(file="somewhere").save()
119 |      pictwo = yield Picture(file="elsewhere").save()
120 |      pictures = [picone, pictwo]
121 |      yield user.pictures.set(pictures)
122 |      
123 |      # now get them
124 |      pictures = yield user.pictures.get()
125 |      print pictures[0].file
126 |      print pictures[1].file
127 | 
128 | addPictures()
129 | 
130 |

Additionally, there is a clear() method that will clear all 131 | objects in a given has many relationship.

132 | 133 |

Belongs To

134 |

135 | A good example of the "belongs to" property is the reverse of the "has many": 136 |

137 |
138 | from twistar.dbobject import DBObject
139 | from twistar.registry import Registry
140 | 
141 | class User(DBObject):
142 |      HASMANY = ['pictures']
143 | 
144 | class Picture(DBObject):
145 |      BELONGSTO = ['user']
146 | 
147 | Registry.register(User, Picture)
148 | 
149 |

150 | Assuming the DB structure is the same as in the "has many" example, you can now get and set the user that 151 | a particular picture belongs to (using get() and set() 152 | methods on picture.user). Additionally, there is a clear() method that will clear all 153 | objects in a given "belongs to" relationship. 154 |

155 |

Has and Belongs To Many

156 |

157 | In a "has and belongs to many" relationship two objects have a many to many relationship. For instance, users and favorite colors. 158 | A user can have many favorite colors and a favorite color could belong to many users. In this example, there should be three tables — 159 | a users table, a favorite_colors table, and a favorite_colors_users table. The last on the list 160 | is a special table that stores the relationships between the colors and users. It has no primary key, and two columns: one for 161 | user_ids and one for favorite_color_ids. The table's name by convention should be the combination of the other 162 | two table names, joined with an underscore, and arranged alphabetically. The classes would look like: 163 |

164 |
165 | from twistar.dbobject import DBObject
166 | from twistar.registry import Registry
167 | 
168 | class User(DBObject):
169 |      HABTM = ['favorite_colors']
170 | 
171 | class FavoriteColor(DBObject):
172 |      HABTM = ['users']
173 | 
174 | Registry.register(User, FavoriteColor)
175 | 
176 |

177 | You can now get and set the users of favorite colors and the favorite colors of users 178 | (using get() and set() 179 | methods). Additionally, there is a clear() method that will clear all 180 | objects in a given relationship. 181 |

182 | 183 |

Polymorphic Relationships

184 |

185 | With a polymorhpic relationship, a model can belong to more than one other model using a single relationship. For instance, take the example 186 | where two models (say, Boy and Girl) both have many nicknames. Each Nickname 187 | can belong to either a Boy or a Girl. In this example, the tables for boys and girls may have whatever 188 | attributes you would like; the nicknames table, though, needs two columns in addition to an id and a value column. These columns are used to identify the id and 189 | type of the other class. In this case, they will be called nicknameable_id and nicknameable_type. 190 |

191 |

192 | The classes look like this: 193 |

194 |
195 | from twistar.dbobject import DBObject
196 | from twistar.registry import Registry
197 | 
198 | class Boy(DBObject):
199 |     HASMANY = [{'name': 'nicknames', 'as': 'nicknameable'}]
200 | 
201 | class Girl(DBObject):
202 |     HASMANY = [{'name': 'nicknames', 'as': 'nicknameable'}]    
203 | 
204 | class Nickname(DBObject):
205 |     BELONGSTO = [{'name': 'nicknameable', 'polymorphic': True}]
206 | 
207 | Registry.register(Boy, Girl, Nickname)
208 | 
209 |

210 | The use is pretty simple, and follows the same form as you would expect with a traditional "belongs to" relationship. 211 |

212 |
213 | def sayHi(nicknameable):
214 |      print "Hello, my name is %s and I am a %s" % (nicknameable.name, nicknameable.__class__.__name__)
215 | 
216 | def getPerson(nickname):
217 |      nickname.nicknameable.get().addCallback(sayHi)
218 | 
219 | def setNickname(nickname, boyOrGirl):
220 |      boyOrGirl.nicknames.set([nickname]).addCallback(lambda _: getPerson(nickname))
221 | 
222 | def boySaved(boy):
223 |      Nickname(value="Bob").save().addCallback(setNickname, boy)
224 | 
225 | def girlSaved(girl):
226 |      Nickname(value="Susie").save().addCallback(setNickname, girl)
227 | 
228 | Boy(name="Robert").save().addCallback(boySaved)
229 | Girl(name="Susan").save().addCallback(girlSaved)
230 | 
231 |

232 | You can see that the nicknames for the boy are set via boy.nicknames.set; they are fetched as you would expect via boy.nicknames.get 233 | (with the same form for girls). For each nickname, by calling nickname.nicknameable.get you could end up with either a Boy or a 234 | Girl. 235 |

236 | 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /doc/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* shamlessly stolen from http://twistedmatrix.com/trac/browser/trunk/doc/core/howto/stylesheet.css?format=txt */ 2 | 3 | body 4 | { 5 | margin-left: 2em; 6 | margin-right: 2em; 7 | border: 0px; 8 | padding: 0px; 9 | font-family: sans-serif; 10 | } 11 | 12 | .done { color: #005500; background-color: #99ff99 } 13 | .notdone { color: #550000; background-color: #ff9999;} 14 | 15 | pre 16 | { 17 | padding: 1em; 18 | border: thin black solid; 19 | line-height: 1.2em; 20 | } 21 | 22 | .boxed 23 | { 24 | padding: 1em; 25 | border: thin black solid; 26 | } 27 | 28 | .shell 29 | { 30 | background-color: #ffffdd; 31 | } 32 | 33 | .python 34 | { 35 | background-color: #dddddd; 36 | } 37 | 38 | .htmlsource 39 | { 40 | background-color: #dddddd; 41 | } 42 | 43 | .py-prototype 44 | { 45 | background-color: #ddddff; 46 | } 47 | 48 | 49 | .python-interpreter 50 | { 51 | background-color: #ddddff; 52 | } 53 | 54 | .doit 55 | { 56 | border: thin blue dashed ; 57 | background-color: #0ef 58 | } 59 | 60 | .py-src-comment 61 | { 62 | color: #1111CC 63 | } 64 | 65 | .py-src-keyword 66 | { 67 | color: #3333CC; 68 | font-weight: bold; 69 | line-height: 1.0em 70 | } 71 | 72 | .py-src-parameter 73 | { 74 | color: #000066; 75 | font-weight: bold; 76 | line-height: 1.0em 77 | } 78 | 79 | .py-src-identifier 80 | { 81 | color: #CC0000 82 | } 83 | 84 | .py-src-string 85 | { 86 | 87 | color: #115511 88 | } 89 | 90 | .py-src-endmarker 91 | { 92 | display: block; /* IE hack; prevents following line from being sucked into the py-listing box. */ 93 | } 94 | 95 | .py-linenumber 96 | { 97 | background-color: #cdcdcd; 98 | float: left; 99 | margin-top: 0px; 100 | width: 4.0em 101 | } 102 | 103 | .py-listing, .html-listing, .listing 104 | { 105 | margin: 1ex; 106 | border: thin solid black; 107 | background-color: #eee; 108 | } 109 | 110 | .py-listing pre, .html-listing pre, .listing pre 111 | { 112 | margin: 0px; 113 | border: none; 114 | border-bottom: thin solid black; 115 | } 116 | 117 | .py-listing .python 118 | { 119 | margin-top: 0; 120 | margin-bottom: 0; 121 | border: none; 122 | border-bottom: thin solid black; 123 | } 124 | 125 | .html-listing .htmlsource 126 | { 127 | margin-top: 0; 128 | margin-bottom: 0; 129 | border: none; 130 | border-bottom: thin solid black; 131 | } 132 | 133 | .caption 134 | { 135 | text-align: center; 136 | padding-top: 0.5em; 137 | padding-bottom: 0.5em; 138 | } 139 | 140 | .filename 141 | { 142 | font-style: italic; 143 | } 144 | 145 | .manhole-output 146 | { 147 | color: blue; 148 | } 149 | 150 | hr 151 | { 152 | display: inline; 153 | } 154 | 155 | ul 156 | { 157 | padding: 0px; 158 | margin: 0px; 159 | margin-left: 1em; 160 | padding-left: 1em; 161 | border-left: 1em; 162 | } 163 | 164 | li 165 | { 166 | padding: 2px; 167 | } 168 | 169 | dt 170 | { 171 | font-weight: bold; 172 | margin-left: 1ex; 173 | } 174 | 175 | dd 176 | { 177 | margin-bottom: 1em; 178 | } 179 | 180 | div.note 181 | { 182 | background-color: #FFFFCC; 183 | margin-top: 1ex; 184 | margin-left: 5%; 185 | margin-right: 5%; 186 | padding-top: 1ex; 187 | padding-left: 5%; 188 | padding-right: 5%; 189 | border: thin black solid; 190 | } 191 | -------------------------------------------------------------------------------- /doc/template.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Twistar Documentation: 6 | 7 | 18 | 19 | 20 | 21 |

22 |

Documentation Index

23 |
24 |
25 | 26 |
27 | 28 |

Documentation Index

29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import absolute_import 3 | from setuptools import setup, find_packages 4 | 5 | from twistar import version 6 | 7 | setup( 8 | name="twistar", 9 | version=version, 10 | description="An implementation of the Active Record pattern for Twisted", 11 | author="Brian Muller", 12 | author_email="bamuller@gmail.com", 13 | license="MIT", 14 | url="http://findingscience.com/twistar", 15 | packages=find_packages(), 16 | install_requires=['twisted >= 12.1','six'] 17 | ) 18 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ; tox configuration file for running tests similar to buildbot builders. 2 | 3 | [tox] 4 | envlist = 5 | py27-twisted12 6 | py27-twisted13 7 | py27-twisted14 8 | py27-twisted15 9 | py35-twisted16 10 | pypy-twisted12 11 | pypy-twisted13 12 | pypy-twisted14 13 | pypy-twisted15 14 | pypy-twisted16 15 | pypy3-twisted16 16 | 17 | [testenv] 18 | deps = 19 | six 20 | coverage 21 | twisted>=15.0, <16.0 22 | commands = 23 | coverage erase 24 | coverage run --source=./twistar {envbindir}/trial --rterrors {posargs:twistar} 25 | 26 | [testenv:py26-twisted12] 27 | deps = 28 | six 29 | coverage 30 | twisted>=12.0, <13.0 31 | 32 | [testenv:py26-twisted13] 33 | deps = 34 | six 35 | coverage 36 | twisted>=13.0, <14.0 37 | 38 | [testenv:py26-twisted14] 39 | deps = 40 | six 41 | coverage 42 | twisted>=14.0, <15.0 43 | 44 | [testenv:py26-twisted15] 45 | deps = 46 | six 47 | coverage 48 | twisted>=15.0, <15.5 49 | 50 | [testenv:py27-twisted12] 51 | deps = 52 | six 53 | coverage 54 | twisted>=12.0, <13.0 55 | 56 | [testenv:py27-twisted13] 57 | deps = 58 | six 59 | coverage 60 | twisted>=13.0, <14.0 61 | 62 | [testenv:py27-twisted14] 63 | deps = 64 | six 65 | coverage 66 | twisted>=14.0, <15.0 67 | 68 | [testenv:py27-twisted15] 69 | deps = 70 | six 71 | coverage 72 | twisted>=15.0, <16.0 73 | 74 | [testenv:py27-twisted16] 75 | deps = 76 | six 77 | coverage 78 | twisted>=16.0, <17.0 79 | 80 | [testenv:py35-twisted16] 81 | deps = 82 | six 83 | coverage 84 | twisted>=16.0, <17.0 85 | 86 | [testenv:pypy-twisted12] 87 | deps = 88 | coverage 89 | twisted>=12.0, <13.0 90 | 91 | [testenv:pypy-twisted13] 92 | deps = 93 | coverage 94 | twisted>=13.0, <14.0 95 | 96 | [testenv:pypy-twisted14] 97 | deps = 98 | coverage 99 | twisted>=14.0, <15.0 100 | 101 | [testenv:pypy-twisted15] 102 | deps = 103 | coverage 104 | twisted>=15.0, <16.0 105 | 106 | [testenv:pypy-twisted16] 107 | deps = 108 | coverage 109 | twisted>=16.0, <17.0 110 | 111 | [testenv:pypy3-twisted16] 112 | deps = 113 | coverage 114 | twisted>=16.0, <17.0 115 | -------------------------------------------------------------------------------- /twistar/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Twistar is a Python implementation of the U{active record pattern} 3 | that uses the U{Twisted } framework's 4 | U{RDBMS support} to provide a non-blocking interface to 5 | relational databases. 6 | 7 | @author: Brian Muller U{bamuller@gmail.com} 8 | """ 9 | from __future__ import absolute_import 10 | version_info = (2, 0) 11 | version = '.'.join(map(str, version_info)) 12 | -------------------------------------------------------------------------------- /twistar/dbconfig/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Package providing interface implementations for interacting with the various 3 | databases. 4 | """ 5 | -------------------------------------------------------------------------------- /twistar/dbconfig/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base module for interfacing with databases. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from twisted.python import log 7 | from twisted.internet import defer 8 | 9 | from twistar.registry import Registry 10 | from twistar.exceptions import ImaginaryTableError, CannotRefreshError 11 | from twistar.utils import joinWheres 12 | from six.moves import range 13 | 14 | 15 | class InteractionBase(object): 16 | """ 17 | Class that specific database implementations extend. 18 | 19 | @cvar LOG: If True, then all queries are logged using C{twisted.python.log.msg}. 20 | 21 | @cvar includeBlankInInsert: If True, then insert/update queries will include 22 | setting object properties that have not be set to null in their respective columns. 23 | """ 24 | 25 | LOG = False 26 | includeBlankInInsert = True 27 | 28 | 29 | def __init__(self): 30 | self.txn = None 31 | 32 | 33 | def log(self, query, args, kwargs): 34 | """ 35 | Log the query and any args or kwargs using C{twisted.python.log.msg} if 36 | C{InteractionBase.LOG} is True. 37 | """ 38 | if not InteractionBase.LOG: 39 | return 40 | log.msg("TWISTAR query: %s" % query) 41 | if len(args) > 0: 42 | if isinstance(args[0], list): # if args contains a list object 43 | log.msg("TWISTAR args: %s" % ",".join(*args)) 44 | else: 45 | log.msg("TWISTAR args: %s" % ",".join(args)) 46 | elif len(kwargs) > 0: 47 | log.msg("TWISTAR kargs: %s" % str(kwargs)) 48 | 49 | 50 | def executeOperation(self, query, *args, **kwargs): 51 | """ 52 | Simply makes same C{twisted.enterprise.dbapi.ConnectionPool.runOperation} call, but 53 | with call to L{log} function. 54 | """ 55 | self.log(query, args, kwargs) 56 | return Registry.DBPOOL.runOperation(query, *args, **kwargs) 57 | 58 | 59 | def execute(self, query, *args, **kwargs): 60 | """ 61 | Simply makes same C{twisted.enterprise.dbapi.ConnectionPool.runQuery} call, but 62 | with call to L{log} function. 63 | """ 64 | self.log(query, args, kwargs) 65 | return Registry.DBPOOL.runQuery(query, *args, **kwargs) 66 | 67 | 68 | def executeTxn(self, txn, query, *args, **kwargs): 69 | """ 70 | Execute given query within the given transaction. Also, makes call 71 | to L{log} function. 72 | """ 73 | self.log(query, args, kwargs) 74 | return txn.execute(query, *args, **kwargs) 75 | 76 | 77 | def select(self, tablename, id=None, where=None, group=None, limit=None, orderby=None, select=None): 78 | """ 79 | Select rows from a table. 80 | 81 | @param tablename: The tablename to select rows from. 82 | 83 | @param id: If given, only the row with the given id will be returned (or C{None} if not found). 84 | 85 | @param where: Conditional of the same form as the C{where} parameter in L{DBObject.find}. 86 | 87 | @param group: String describing how to group results. 88 | 89 | @param limit: Integer limit on the number of results. If this value is 1, then the result 90 | will be a single dictionary. Otherwise, if C{id} is not specified, an array will be returned. 91 | This can also be a tuple, where the first value is the integer limit and the second value is 92 | an integer offset. In the case that an offset is specified, an array will always be returned. 93 | 94 | @param orderby: String describing how to order the results. 95 | 96 | @param select: Columns to select. Default is C{*}. 97 | 98 | @return: If C{limit} is 1 or id is set, then the result is one dictionary or None if not found. 99 | Otherwise, an array of dictionaries are returned. 100 | """ 101 | one = False 102 | cacheTableStructure = select is None 103 | select = select or "*" 104 | 105 | if id is not None: 106 | if where is None: 107 | where = ["id = ?", id] 108 | else: 109 | where = joinWheres(where, ["id = ?", id]) 110 | one = True 111 | 112 | if not isinstance(limit, tuple) and limit is not None and int(limit) == 1: 113 | one = True 114 | 115 | q = "SELECT %s FROM %s" % (select, tablename) 116 | args = [] 117 | if where is not None: 118 | wherestr, args = self.whereToString(where) 119 | q += " WHERE " + wherestr 120 | if group is not None: 121 | q += " GROUP BY " + group 122 | if orderby is not None: 123 | q += " ORDER BY " + orderby 124 | 125 | if isinstance(limit, tuple): 126 | q += " LIMIT %s OFFSET %s" % (limit[0], limit[1]) 127 | elif limit is not None: 128 | q += " LIMIT " + str(limit) 129 | 130 | return self.runInteraction(self._doselect, q, args, tablename, one, cacheTableStructure) 131 | 132 | 133 | def _doselect(self, txn, q, args, tablename, one=False, cacheable=True): 134 | """ 135 | Private callback for actual select query call. 136 | 137 | @param cacheable Denotes whether or not we can use the results of this 138 | query to keep the structure of a table on hand. 139 | """ 140 | self.executeTxn(txn, q, args) 141 | 142 | if one: 143 | result = txn.fetchone() 144 | if not result: 145 | return None 146 | vals = self.valuesToHash(txn, result, tablename, cacheable) 147 | return vals 148 | 149 | results = [] 150 | for result in txn.fetchall(): 151 | vals = self.valuesToHash(txn, result, tablename, cacheable) 152 | results.append(vals) 153 | return results 154 | 155 | 156 | def insertArgsToString(self, vals): 157 | """ 158 | Convert C{{'name': value}} to an insert "values" string like C{"(%s,%s,%s)"}. 159 | """ 160 | return "(" + ",".join(["%s" for _ in vals.items()]) + ")" 161 | 162 | 163 | def insert(self, tablename, vals, txn=None): 164 | """ 165 | Insert a row into the given table. 166 | 167 | @param tablename: Table to insert a row into. 168 | 169 | @param vals: Values to insert. Should be a dictionary in the form of 170 | C{{'name': value, 'othername': value}}. 171 | 172 | @param txn: If txn is given it will be used for the query, 173 | otherwise a typical runQuery will be used 174 | 175 | @return: A C{Deferred} that calls a callback with the id of new row. 176 | """ 177 | params = self.insertArgsToString(vals) 178 | colnames = "" 179 | if len(vals) > 0: 180 | ecolnames = self.escapeColNames(vals.keys()) 181 | colnames = "(" + ",".join(ecolnames) + ")" 182 | params = "VALUES %s" % params 183 | q = "INSERT INTO %s %s %s" % (tablename, colnames, params) 184 | 185 | # if we have a transaction use it 186 | if txn is not None: 187 | self.executeTxn(txn, q, list(vals.values())) 188 | return self.getLastInsertID(txn) 189 | 190 | def _insert(txn, q, vals): 191 | self.executeTxn(txn, q, list(vals.values())) 192 | return self.getLastInsertID(txn) 193 | return self.runInteraction(_insert, q, vals) 194 | 195 | 196 | def escapeColNames(self, colnames): 197 | """ 198 | Escape column names for insertion into SQL statement. 199 | 200 | @param colnames: A C{List} of string column names. 201 | 202 | @return: A C{List} of string escaped column names. 203 | """ 204 | return ["`%s`" % x for x in colnames] 205 | 206 | 207 | def insertMany(self, tablename, vals): 208 | """ 209 | Insert many values into a table. 210 | 211 | @param tablename: Table to insert a row into. 212 | 213 | @param vals: Values to insert. Should be a list of dictionaries in the form of 214 | C{{'name': value, 'othername': value}}. 215 | 216 | @return: A C{Deferred}. 217 | """ 218 | colnames = ",".join(self.escapeColNames(vals[0].keys())) 219 | params = ",".join([self.insertArgsToString(val) for val in vals]) 220 | args = [] 221 | for val in vals: 222 | args = args + list(val.values()) 223 | q = "INSERT INTO %s (%s) VALUES %s" % (tablename, colnames, params) 224 | return self.executeOperation(q, args) 225 | 226 | 227 | def getLastInsertID(self, txn): 228 | """ 229 | Using the given txn, get the id of the last inserted row. 230 | 231 | @return: The integer id of the last inserted row. 232 | """ 233 | return txn.lastrowid 234 | 235 | 236 | def delete(self, tablename, where=None): 237 | """ 238 | Delete from the given tablename. 239 | 240 | @param where: Conditional of the same form as the C{where} parameter in L{DBObject.find}. 241 | If given, the rows deleted will be restricted to ones matching this conditional. 242 | 243 | @return: A C{Deferred}. 244 | """ 245 | q = "DELETE FROM %s" % tablename 246 | args = [] 247 | if where is not None: 248 | wherestr, args = self.whereToString(where) 249 | q += " WHERE " + wherestr 250 | return self.executeOperation(q, args) 251 | 252 | 253 | def update(self, tablename, args, where=None, txn=None, limit=None): 254 | """ 255 | Update a row into the given table. 256 | 257 | @param tablename: Table to insert a row into. 258 | 259 | @param args: Values to insert. Should be a dictionary in the form of 260 | C{{'name': value, 'othername': value}}. 261 | 262 | @param where: Conditional of the same form as the C{where} parameter in L{DBObject.find}. 263 | If given, the rows updated will be restricted to ones matching this conditional. 264 | 265 | @param txn: If txn is given it will be used for the query, 266 | otherwise a typical runQuery will be used 267 | 268 | @param limit: If limit is given it will limit the number of rows that are updated. 269 | 270 | @return: A C{Deferred} 271 | """ 272 | setstring, args = self.updateArgsToString(args) 273 | q = "UPDATE %s " % tablename + " SET " + setstring 274 | if where is not None: 275 | wherestr, whereargs = self.whereToString(where) 276 | q += " WHERE " + wherestr 277 | args += whereargs 278 | if limit is not None: 279 | q += " LIMIT " + str(limit) 280 | 281 | if txn is not None: 282 | return self.executeTxn(txn, q, args) 283 | return self.executeOperation(q, args) 284 | 285 | 286 | def valuesToHash(self, txn, values, tablename, cacheable=True): 287 | """ 288 | Given a row from a database query (values), create 289 | a hash using keys from the table schema and values from 290 | the given values; 291 | 292 | @param txn: The transaction to use for the schema update query. 293 | 294 | @param values: A row from a db (as a C{list}). 295 | 296 | @param tablename: Name of the table to fetch the schema for. 297 | 298 | @param cacheable: Can the resulting table structure be cached for 299 | future reference? 300 | """ 301 | cols = [row[0] for row in txn.description] 302 | if cacheable and tablename not in Registry.SCHEMAS: 303 | Registry.SCHEMAS[tablename] = cols 304 | h = {} 305 | for index in range(len(values)): 306 | colname = cols[index] 307 | h[colname] = values[index] 308 | return h 309 | 310 | 311 | def getSchema(self, tablename, txn=None): 312 | """ 313 | Get the schema (in the form of a list of column names) for 314 | a given tablename. Use the given transaction if specified. 315 | """ 316 | if tablename not in Registry.SCHEMAS and txn is not None: 317 | try: 318 | self.executeTxn(txn, "SELECT * FROM %s LIMIT 1" % tablename) 319 | except Exception: 320 | raise ImaginaryTableError("Table %s does not exist." % tablename) 321 | Registry.SCHEMAS[tablename] = [row[0] for row in txn.description] 322 | return Registry.SCHEMAS.get(tablename, []) 323 | 324 | 325 | def runInteraction(self, interaction, *args, **kwargs): 326 | if self.txn is not None: 327 | return defer.succeed(interaction(self.txn, *args, **kwargs)) 328 | return Registry.DBPOOL.runInteraction(interaction, *args, **kwargs) 329 | 330 | 331 | def insertObj(self, obj): 332 | """ 333 | Insert the given object into its table. 334 | 335 | @return: A C{Deferred} that sends a callback the inserted object. 336 | """ 337 | def _doinsert(txn): 338 | klass = obj.__class__ 339 | tablename = klass.tablename() 340 | cols = self.getSchema(tablename, txn) 341 | if len(cols) == 0: 342 | raise ImaginaryTableError("Table %s does not exist." % tablename) 343 | vals = obj.toHash(cols, includeBlank=self.__class__.includeBlankInInsert, exclude=['id']) 344 | self.insert(tablename, vals, txn) 345 | obj.id = self.getLastInsertID(txn) 346 | return obj 347 | 348 | return self.runInteraction(_doinsert) 349 | 350 | 351 | def updateObj(self, obj): 352 | """ 353 | Update the given object's row in the object's table. 354 | 355 | @return: A C{Deferred} that sends a callback the updated object. 356 | """ 357 | def _doupdate(txn): 358 | klass = obj.__class__ 359 | tablename = klass.tablename() 360 | cols = self.getSchema(tablename, txn) 361 | 362 | vals = obj.toHash(cols, includeBlank=True, exclude=['id']) 363 | return self.update(tablename, vals, where=['id = ?', obj.id], txn=txn) 364 | # We don't want to return the cursor - so add a blank callback returning the obj 365 | return self.runInteraction(_doupdate).addCallback(lambda _: obj) 366 | 367 | 368 | def refreshObj(self, obj): 369 | """ 370 | Update the given object based on the information in the object's table. 371 | 372 | @return: A C{Deferred} that sends a callback the updated object. 373 | """ 374 | def _dorefreshObj(newobj): 375 | if obj is None: 376 | raise CannotRefreshError("Can't refresh object if id not longer exists.") 377 | for key in newobj.keys(): 378 | setattr(obj, key, newobj[key]) 379 | return self.select(obj.tablename(), obj.id).addCallback(_dorefreshObj) 380 | 381 | 382 | def whereToString(self, where): 383 | """ 384 | Convert a conditional to the form needed for a query using the DBAPI. For instance, 385 | for most DB's question marks in the query string have to be converted to C{%s}. This 386 | will vary by database. 387 | 388 | @param where: Conditional of the same form as the C{where} parameter in L{DBObject.find}. 389 | 390 | @return: A conditional in the same form as the C{where} parameter in L{DBObject.find}. 391 | """ 392 | assert(isinstance(where, list)) 393 | query = where[0].replace("?", "%s") 394 | args = where[1:] 395 | return (query, args) 396 | 397 | 398 | def updateArgsToString(self, args): 399 | """ 400 | Convert dictionary of arguments to form needed for DB update query. This method will 401 | vary by database driver. 402 | 403 | @param args: Values to insert. Should be a dictionary in the form of 404 | C{{'name': value, 'othername': value}}. 405 | 406 | @return: A tuple of the form C{('name = %s, othername = %s, ...', argvalues)}. 407 | """ 408 | colnames = self.escapeColNames(list(args.keys())) 409 | setstring = ",".join([key + " = %s" for key in colnames]) 410 | return (setstring, list(args.values())) 411 | 412 | 413 | def count(self, tablename, where=None): 414 | """ 415 | Get the number of rows in the given table (optionally, that meet the given where criteria). 416 | 417 | @param tablename: The tablename to count rows from. 418 | 419 | @param where: Conditional of the same form as the C{where} parameter in L{DBObject.find}. 420 | 421 | @return: A C{Deferred} that returns the number of rows. 422 | """ 423 | d = self.select(tablename, where=where, select='count(*)') 424 | d.addCallback(lambda res: res[0]['count(*)']) 425 | return d 426 | -------------------------------------------------------------------------------- /twistar/dbconfig/mysql.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import MySQLdb 3 | 4 | from twisted.enterprise import adbapi 5 | from twisted.python import log 6 | 7 | from twistar.dbconfig.base import InteractionBase 8 | 9 | 10 | class MySQLDBConfig(InteractionBase): 11 | includeBlankInInsert = False 12 | 13 | def insertArgsToString(self, vals): 14 | if len(vals) > 0: 15 | return "(" + ",".join(["%s" for _ in vals.items()]) + ")" 16 | return "VALUES ()" 17 | 18 | 19 | class ReconnectingMySQLConnectionPool(adbapi.ConnectionPool): 20 | """ 21 | This connection pool will reconnect if the server goes away. This idea was taken from: 22 | http://www.gelens.org/2009/09/13/twisted-connectionpool-revisited/ 23 | """ 24 | def _runInteraction(self, interaction, *args, **kw): 25 | try: 26 | return adbapi.ConnectionPool._runInteraction(self, interaction, *args, **kw) 27 | except MySQLdb.OperationalError as e: 28 | if e[0] not in (2006, 2013): 29 | raise 30 | log.err("Lost connection to MySQL, retrying operation. If no errors follow, retry was successful.") 31 | conn = self.connections.get(self.threadID()) 32 | self.disconnect(conn) 33 | return adbapi.ConnectionPool._runInteraction(self, interaction, *args, **kw) 34 | -------------------------------------------------------------------------------- /twistar/dbconfig/postgres.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twistar.dbconfig.base import InteractionBase 3 | 4 | 5 | class PostgreSQLDBConfig(InteractionBase): 6 | includeBlankInInsert = False 7 | 8 | def getLastInsertID(self, txn): 9 | q = "SELECT lastval()" 10 | self.executeTxn(txn, q) 11 | result = txn.fetchall() 12 | return result[0][0] 13 | 14 | 15 | def insertArgsToString(self, vals): 16 | if len(vals) > 0: 17 | return "(" + ",".join(["%s" for _ in vals.items()]) + ")" 18 | return "DEFAULT VALUES" 19 | 20 | 21 | def escapeColNames(self, colnames): 22 | return ['"%s"' % x for x in colnames] 23 | 24 | 25 | def count(self, tablename, where=None): 26 | d = self.select(tablename, where=where, select='count(*)') 27 | d.addCallback(lambda res: res[0]['count']) 28 | return d 29 | -------------------------------------------------------------------------------- /twistar/dbconfig/pyodbc.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twistar.dbconfig.base import InteractionBase 3 | 4 | 5 | class PyODBCDBConfig(InteractionBase): 6 | 7 | def whereToString(self, where): 8 | assert(isinstance(where, list)) 9 | query = where[0] 10 | args = where[1:] 11 | return (query, args) 12 | 13 | 14 | def updateArgsToString(self, args): 15 | colnames = self.escapeColNames(args.keys()) 16 | setstring = ",".join([key + " = ?" for key in colnames]) 17 | return (setstring, list(args.values())) 18 | 19 | 20 | def insertArgsToString(self, vals): 21 | return "(" + ",".join(["?" for _ in vals.items()]) + ")" 22 | -------------------------------------------------------------------------------- /twistar/dbconfig/sqlite.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twistar.registry import Registry 3 | from twistar.dbconfig.base import InteractionBase 4 | 5 | 6 | class SQLiteDBConfig(InteractionBase): 7 | def whereToString(self, where): 8 | assert(isinstance(where, list)) 9 | query = where[0] 10 | args = where[1:] 11 | return (query, args) 12 | 13 | 14 | def updateArgsToString(self, args): 15 | colnames = self.escapeColNames(args.keys()) 16 | setstring = ",".join([key + " = ?" for key in colnames]) 17 | return (setstring, list(args.values())) 18 | 19 | 20 | def insertArgsToString(self, vals): 21 | return "(" + ",".join(["?" for _ in vals.items()]) + ")" 22 | 23 | 24 | # retarded sqlite can't handle multiple row inserts 25 | def insertMany(self, tablename, vals): 26 | def _insertMany(txn): 27 | for val in vals: 28 | self.insert(tablename, val, txn) 29 | return Registry.DBPOOL.runInteraction(_insertMany) 30 | -------------------------------------------------------------------------------- /twistar/dbobject.py: -------------------------------------------------------------------------------- 1 | """ 2 | Code relating to the base L{DBObject} object. 3 | """ 4 | from __future__ import absolute_import 5 | from twisted.internet import defer 6 | 7 | from twistar.registry import Registry 8 | from twistar.relationships import Relationship 9 | from twistar.exceptions import InvalidRelationshipError, DBObjectSaveError, ReferenceNotSavedError 10 | from twistar.utils import createInstances, deferredDict, dictToWhere, transaction 11 | from twistar.validation import Validator, Errors 12 | 13 | from BermiInflector.Inflector import Inflector 14 | import six 15 | 16 | 17 | class DBObject(Validator): 18 | """ 19 | A base class for representing objects stored in a RDBMS. 20 | 21 | @cvar HASMANY: A C{list} made up of some number of strings and C{dict}s. If an element is a string, 22 | it represents what the class has many of, for instance C{'users'}. If an element is a C{dict}, then 23 | it should minimally have a C{name} attribute (with a value the same as if the element were a string) 24 | and then any additional options. See L{Relationship} and L{HasMany} for more information. 25 | 26 | @cvar HASONE: A C{list} made up of some number of strings and C{dict}s. If an element is a string, 27 | it represents what the class has one of, for instance C{'location'}. If an element is a C{dict}, then 28 | it should minimally have a C{name} attribute (with a value the same as if the element were a string) 29 | and then any additional options. See L{Relationship} and L{HasOne} for more information. 30 | 31 | @cvar HABTM: A C{list} made up of some number of strings and C{dict}s. If an element is a string, 32 | it represents what the class has many of (and which in turn has many of this current object type), 33 | for instance a teacher has and belongs to many students. Both the C{Student} and C{Teacher} classes 34 | should have a class variable that is C{HABTM = ['teachers']} and C{HABTM = ['students']}, respectively. 35 | If an element is a C{dict}, then 36 | it should minimally have a C{name} attribute (with a value the same as if the element were a string) 37 | and then any additional options. See L{Relationship} and L{HABTM} for more information. 38 | 39 | @cvar BELONGSTO: A C{list} made up of some number of strings and C{dict}s. If an element is a string, 40 | it represents what the class belongs to, for instance C{'user'}. If an element is a C{dict}, then 41 | it should minimally have a C{name} attribute (with a value the same as if the element were a string) 42 | and then any additional options. See L{Relationship} and L{BelongsTo} for more information. 43 | 44 | @cvar TABLENAME: If specified, use the given tablename as the one for this object. Otherwise, 45 | use the lowercase, plural version of this class's name. See the L{DBObject.tablename} 46 | method. 47 | 48 | @see: L{Relationship}, L{HasMany}, L{HasOne}, L{HABTM}, L{BelongsTo} 49 | """ 50 | 51 | HASMANY = [] 52 | HASONE = [] 53 | HABTM = [] 54 | BELONGSTO = [] 55 | 56 | # this will just be a hash of relationships for faster property resolution 57 | # the keys are the name and the values are classes representing the relationship 58 | # it will be of the form {'othername': , 'anothername': } 59 | RELATIONSHIP_CACHE = None 60 | 61 | def __init__(self, **kwargs): 62 | """ 63 | Constructor. DO NOT OVERWRITE. Use the L{DBObject.afterInit} method. 64 | 65 | @param kwargs: An optional dictionary containing the properties that 66 | should be initially set for this object. 67 | 68 | @see: L{DBObject.afterInit} 69 | """ 70 | self.id = None 71 | self._deleted = False 72 | self.errors = Errors() 73 | self.updateAttrs(kwargs) 74 | self._config = Registry.getConfig() 75 | 76 | if self.__class__.RELATIONSHIP_CACHE is None: 77 | self.__class__.initRelationshipCache() 78 | 79 | 80 | def updateAttrs(self, kwargs): 81 | """ 82 | Set the attributes of this object based on the given C{dict}. 83 | 84 | @param kwargs: A C{dict} whose keys will be turned into properties and whose values 85 | will then be assigned to those properties. 86 | """ 87 | for k, v in six.iteritems(kwargs): 88 | setattr(self, k, v) 89 | 90 | 91 | def save(self): 92 | """ 93 | Save this object to the database. Validation is performed first; if the 94 | validation fails (that is, if C{obj.errors.isEmpty()} is False) then the 95 | object will not be saved. To test for errors, use C{obj.errors.isEmpty()}. 96 | 97 | @return: A C{Deferred} object. If a callback is added to that deferred 98 | the value of the saved (or unsaved if there are errors) object will be returned. 99 | 100 | @see: L{Validator}, L{Errors} 101 | """ 102 | if self._deleted: 103 | raise DBObjectSaveError("Cannot save a previously deleted object.") 104 | 105 | def _save(isValid): 106 | if self.id is None and isValid: 107 | return self._create() 108 | elif isValid: 109 | return self._update() 110 | return self 111 | return self.isValid().addCallback(_save) 112 | 113 | 114 | def validate(self): 115 | """ 116 | Run all validations associated with this object's class. This will return a deferred 117 | (actually a C{DeferredList}). When this deferred is finished, this object's errors 118 | dictionary property will either be empty or will contain the errors from this object 119 | (keys are property names, values are the error messages describing the error). 120 | """ 121 | return self.__class__._validate(self) 122 | 123 | 124 | def isValid(self): 125 | """ 126 | This method first calls L{validate} and then returns a deferred that returns True if 127 | there were no errors and False otherwise. 128 | """ 129 | def _isValid(obj): 130 | return obj.errors.isEmpty() 131 | return self.validate().addCallback(_isValid) 132 | 133 | 134 | def beforeCreate(self): 135 | """ 136 | Method called before a new object is created. Classes can overwrite this method. 137 | If False is returned, then the object is not saved in the database. This method 138 | may return a C{Deferred}. 139 | """ 140 | 141 | 142 | def beforeUpdate(self): 143 | """ 144 | Method called before an existing object is updated. Classes can overwrite this method. 145 | If False is returned, then the object is not saved in the database. This method 146 | may return a C{Deferred}. 147 | """ 148 | 149 | 150 | def beforeSave(self): 151 | """ 152 | Method called before an object is saved. Classes can overwrite this method. 153 | If False is returned, then the object is not saved in the database. This method 154 | may return a C{Deferred}. 155 | 156 | This method is called after L{beforeCreate} when an object is being created, and after 157 | L{beforeUpdate} when an existing object (whose C{id} is not C{None}) is being saved. 158 | """ 159 | 160 | 161 | def afterInit(self): 162 | """ 163 | Method called when a new L{DBObject} is instantiated as a result of DB queries. If you 164 | create an instance of this class on your own, you will need to call the method yourself. 165 | Classes can overwrite this method. This method may return a C{Deferred}. 166 | """ 167 | 168 | 169 | def beforeDelete(self): 170 | """ 171 | Method called before a L{DBObject} is deleted. Classes can overwrite this method. 172 | If False is returned, then the L{DBObject} is not deleted from database. 173 | This method may return a C{Deferred}. 174 | """ 175 | 176 | 177 | def _create(self): 178 | """ 179 | Method to actually create an object in the DB. Handles calling this class's 180 | L{beforeCreate} followed by it's L{beforeSave} method. 181 | 182 | @return: A C{Deferred} object. If a callback is added to that deferred 183 | the value of the saved object will be returned (unless the L{beforeCreate} 184 | or L{beforeSave} methods returns C{False}, in which case the unsaved object 185 | will be returned). 186 | """ 187 | def _createOnSuccess(result): 188 | if result is False: 189 | return defer.succeed(self) 190 | return self._config.insertObj(self) 191 | 192 | def _beforeSave(result): 193 | if result is False: 194 | return defer.succeed(self) 195 | return defer.maybeDeferred(self.beforeSave).addCallback(_createOnSuccess) 196 | 197 | return defer.maybeDeferred(self.beforeCreate).addCallback(_beforeSave) 198 | 199 | 200 | def _update(self): 201 | """ 202 | Method to actually save an existing object in the DB. Handles calling this class's 203 | L{beforeUpdate} and L{beforeSave} methods. 204 | 205 | @return: A C{Deferred} object. If a callback is added to that deferred 206 | the value of the saved object will be returned (unless the L{beforeUpdate} 207 | or L{beforeSave} methods returns C{False}, in which case the unsaved 208 | object will be returned). 209 | """ 210 | def _saveOnSuccess(result): 211 | if result is False: 212 | return defer.succeed(self) 213 | return self._config.updateObj(self) 214 | 215 | def _beforeSave(result): 216 | if result is False: 217 | return defer.succeed(self) 218 | return defer.maybeDeferred(self.beforeSave).addCallback(_saveOnSuccess) 219 | 220 | return defer.maybeDeferred(self.beforeUpdate).addCallback(_beforeSave) 221 | 222 | 223 | def refresh(self): 224 | """ 225 | Update the properties for this object from the database. 226 | 227 | @return: A C{Deferred} object. 228 | """ 229 | return self._config.refreshObj(self) 230 | 231 | 232 | def toHash(self, cols, includeBlank=False, exclude=None, base=None): 233 | """ 234 | Convert this object to a dictionary. 235 | 236 | @param includeBlank: Boolean representing whether or not properties that 237 | have not been set should be included (the initial property list is retrieved 238 | from the schema of the database for the given class's schema). 239 | 240 | @param exclue: A C{list} of properties to ignore when creating the C{dict} to 241 | return. 242 | 243 | @param base: An initial base C{dict} to add this objects properties to. 244 | 245 | @return: A C{dict} formed from the properties and values of this object. 246 | """ 247 | exclude = exclude or [] 248 | h = base or {} 249 | for col in cols: 250 | if col in exclude: 251 | continue 252 | value = getattr(self, col, None) 253 | if (value is not None or includeBlank): 254 | h[col] = value 255 | return h 256 | 257 | 258 | def delete(self): 259 | """ 260 | Delete this instance from the database. Calls L{beforeDelete} before deleting from 261 | the database. 262 | 263 | @return: A C{Deferred}. 264 | """ 265 | 266 | def _delete(result): 267 | oldid = self.id 268 | self.id = None 269 | self._deleted = True 270 | return self.__class__.deleteAll(where=["id = ?", oldid]) 271 | 272 | def _deleteOnSuccess(result): 273 | if result is False: 274 | return defer.succeed(self) 275 | else: 276 | ds = [] 277 | for relation in self.HABTM: 278 | name = relation['name'] if isinstance(relation, dict) else relation 279 | ds.append(getattr(self, name).clear()) 280 | return defer.DeferredList(ds).addCallback(_delete) 281 | 282 | return defer.maybeDeferred(self.beforeDelete).addCallback(_deleteOnSuccess) 283 | 284 | 285 | def loadRelations(self, *relations): 286 | """ 287 | Preload a a list of relationships. For instance, if you have an instance of an 288 | object C{User} (named C{user}) that has many C{Address}es and has one C{Avatar}, 289 | you could call C{user.loadRelations('addresses', 'avatar').addCallback('handleUser')} 290 | instead of having to call C{user.addresses.get()} and C{user.avatar.get()} and assign 291 | callbacks to the results of those calls. In the first case, the function C{handleUser} 292 | would accept one argument, which will be a dictionary whose keys are the property names 293 | and whose values are the results of the C{get()} calls. This just makes it easier to 294 | load multiple properties at once, without having to create a long list of callbacks. 295 | 296 | If the method is called without any arguments, then all relations will loaded. 297 | 298 | @return: A C{Deferred}. 299 | """ 300 | if len(relations) == 0: 301 | klass = object.__getattribute__(self, "__class__") 302 | allrelations = list(klass.RELATIONSHIP_CACHE.keys()) 303 | if len(allrelations) == 0: 304 | return defer.succeed({}) 305 | return self.loadRelations(*allrelations) 306 | 307 | ds = {} 308 | for relation in relations: 309 | ds[relation] = getattr(self, relation).get() 310 | return deferredDict(ds) 311 | 312 | 313 | @classmethod 314 | def addRelation(klass, relation, rtype): 315 | """ 316 | Add a relationship to the given Class. 317 | 318 | @param klass: The class extending this one. 319 | 320 | @param relation: Either a string with the name of property to create 321 | for this class or a dictionary decribing the relationship. For instance, 322 | if a User L{HasMany} Pictures then the relation could either by 'pictures' 323 | or a dictionary with at least one "name" key, as in 324 | C{{'name': 'pictures', ...}} along with other options. 325 | 326 | @param rtype: The relationship type. It should be a key value from 327 | the C{TYPES} class variable in the class L{Relationship}. 328 | """ 329 | if isinstance(relation, dict): 330 | if 'name' not in relation: 331 | msg = "No key 'name' in the relation %s in class %s" % (relation, klass.__name__) 332 | raise InvalidRelationshipError(msg) 333 | name = relation['name'] 334 | args = relation 335 | else: 336 | name = relation 337 | args = {} 338 | relationshipKlass = Relationship.TYPES[rtype] 339 | klass.RELATIONSHIP_CACHE[name] = (relationshipKlass, args) 340 | 341 | 342 | @classmethod 343 | def initRelationshipCache(klass): 344 | """ 345 | Initialize the cache of relationship objects for this class. 346 | """ 347 | klass.RELATIONSHIP_CACHE = {} 348 | for rtype in Relationship.TYPES.keys(): 349 | for relation in getattr(klass, rtype): 350 | klass.addRelation(relation, rtype) 351 | 352 | 353 | @classmethod 354 | def tablename(klass): 355 | """ 356 | Get the tablename for the given class. If the class has a C{TABLENAME} 357 | variable then that will be used - otherwise, it is is inferred from the 358 | class name. 359 | 360 | @param klass: The class to get the tablename for. 361 | """ 362 | if not hasattr(klass, 'TABLENAME'): 363 | inf = Inflector() 364 | klass.TABLENAME = inf.tableize(klass.__name__) 365 | return klass.TABLENAME 366 | 367 | 368 | @classmethod 369 | def findOrCreate(klass, **attrs): 370 | """ 371 | Find all instances of a given class based on the attributes given (just like C{findBy}). 372 | 373 | If a match isn't found, create a new instance and return that. 374 | """ 375 | @transaction 376 | def _findOrCreate(trans): 377 | def handle(result): 378 | if len(result) == 0: 379 | return klass(**attrs).save() 380 | return result[0] 381 | return klass.findBy(**attrs).addCallback(handle) 382 | return _findOrCreate() 383 | 384 | 385 | @classmethod 386 | def findBy(klass, **attrs): 387 | """ 388 | Find all instances of the given class based on an exact match of attributes. 389 | 390 | For instance: 391 | C{User.find(first_name='Bob', last_name='Smith')} 392 | 393 | Will return all matches. 394 | """ 395 | where = dictToWhere(attrs) 396 | return klass.find(where=where) 397 | 398 | 399 | @classmethod 400 | def find(klass, id=None, where=None, group=None, limit=None, orderby=None): 401 | """ 402 | Find instances of a given class. 403 | 404 | @param id: The integer of the C{klass} to find. For instance, C{Klass.find(1)} 405 | will return an instance of Klass from the row with an id of 1 (unless it isn't 406 | found, in which case C{None} is returned). 407 | 408 | @param where: A C{list} whose first element is the string version of the 409 | condition with question marks in place of any parameters. Further elements 410 | of the C{list} should be the values of any parameters specified. For instance, 411 | C{['first_name = ? AND age > ?', 'Bob', 21]}. 412 | 413 | @param group: A C{str} describing the grouping, like C{group='first_name'}. 414 | 415 | @param limit: An C{int} specifying the limit of the results. If this is 1, 416 | then the return value will be either an instance of C{klass} or C{None}. 417 | 418 | @param orderby: A C{str} describing the ordering, like C{orderby='first_name DESC'}. 419 | 420 | @return: A C{Deferred} which returns the following to a callback: 421 | If id is specified (or C{limit} is 1) then a single 422 | instance of C{klass} will be returned if one is found that fits the criteria, C{None} 423 | otherwise. If id is not specified and C{limit} is not 1, then a C{list} will 424 | be returned with all matching results. 425 | """ 426 | config = Registry.getConfig() 427 | d = config.select(klass.tablename(), id, where, group, limit, orderby) 428 | return d.addCallback(createInstances, klass) 429 | 430 | 431 | @classmethod 432 | def count(klass, where=None): 433 | """ 434 | Count instances of a given class. 435 | 436 | @param where: An optional C{list} whose first element is the string version of the 437 | condition with question marks in place of any parameters. Further elements 438 | of the C{list} should be the values of any parameters specified. For instance, 439 | C{['first_name = ? AND age > ?', 'Bob', 21]}. 440 | 441 | @return: A C{Deferred} which returns the total number of db records to a callback. 442 | """ 443 | config = Registry.getConfig() 444 | return config.count(klass.tablename(), where=where) 445 | 446 | 447 | @classmethod 448 | def all(klass): 449 | """ 450 | Get all instances of the given class in the database. Note that this is the 451 | equivalent of calling L{find} with no arguments. 452 | 453 | @return: A C{Deferred} which returns the following to a callback: 454 | A C{list} containing all of the instances in the database. 455 | """ 456 | return klass.find() 457 | 458 | 459 | @classmethod 460 | def deleteAll(klass, where=None): 461 | """ 462 | Delete all instances of C{klass} in the database without instantiating the records 463 | first or invoking callbacks (L{beforeDelete} is not called). This will run a single 464 | SQL DELETE statement in the database. 465 | 466 | @param where: Conditionally delete instances. This parameter is of the same form 467 | found in L{find}. 468 | 469 | @return: A C{Deferred}. 470 | """ 471 | config = Registry.getConfig() 472 | tablename = klass.tablename() 473 | return config.delete(tablename, where) 474 | 475 | 476 | @classmethod 477 | def exists(klass, where=None): 478 | """ 479 | Find whether or not at least one instance of the given C{klass} exists, optionally 480 | with specific conditions specified in C{where}. 481 | 482 | @param where: Conditionally find instances. This parameter is of the same form 483 | found in L{find}. 484 | 485 | @return: A C{Deferred} which returns the following to a callback: 486 | A boolean as to whether or not at least one object was found. 487 | """ 488 | def _exists(result): 489 | return result is not None 490 | return klass.find(where=where, limit=1).addCallback(_exists) 491 | 492 | 493 | def __str__(self): 494 | """ 495 | Get the string version of this object. 496 | """ 497 | tablename = self.tablename() 498 | attrs = {} 499 | for key in Registry.SCHEMAS.get(tablename, []): 500 | attrs[key] = getattr(self, key, None) 501 | return "<%s object: %s>" % (self.__class__.__name__, str(attrs)) 502 | 503 | 504 | def __getattribute__(self, name): 505 | """ 506 | Get the given attribute. 507 | 508 | @param name: The name of the property to get. 509 | 510 | @return: If the name is a relationship based property, then a 511 | L{Relationship} instance will be returned. Otherwise the set property 512 | of the class will be returned. 513 | """ 514 | klass = object.__getattribute__(self, "__class__") 515 | if klass.RELATIONSHIP_CACHE is not None and name in klass.RELATIONSHIP_CACHE: 516 | if object.__getattribute__(self, 'id') is None: 517 | raise ReferenceNotSavedError("Cannot get/set relationship on unsaved object") 518 | relationshipKlass, args = klass.RELATIONSHIP_CACHE[name] 519 | return relationshipKlass(self, name, args) 520 | return object.__getattribute__(self, name) 521 | 522 | 523 | def __eq__(self, other): 524 | """ 525 | Determine if this object is the same as another (only taking 526 | the type of the other class and it's C{id} into account). 527 | 528 | @param other: The other object to compare this one to. 529 | 530 | @return: A boolean. 531 | """ 532 | eqclass = self.__class__.__name__ == other.__class__.__name__ 533 | eqid = hasattr(other, 'id') and self.id == other.id 534 | return eqclass and eqid 535 | 536 | 537 | def __neq__(self, other): 538 | """ 539 | Determine if this object is not the same as another (only taking 540 | the type of the other class and it's C{id} into account). 541 | 542 | @param other: The other object to compare this one to. 543 | 544 | @return: A boolean. 545 | """ 546 | return not self == other 547 | 548 | 549 | def __hash__(self): 550 | return hash('%s.%d' % (type(self).__name__, self.id)) 551 | 552 | 553 | __repr__ = __str__ 554 | 555 | 556 | Registry.register(DBObject) 557 | -------------------------------------------------------------------------------- /twistar/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | All C{Exception} classes. 3 | """ 4 | 5 | 6 | class TransactionError(Exception): 7 | """ 8 | Error while running a transaction. 9 | """ 10 | 11 | 12 | class ClassNotRegisteredError(Exception): 13 | """ 14 | Error resulting from the attempted fetching of a class from the L{Registry} that was 15 | never registered. 16 | """ 17 | 18 | 19 | class ImaginaryTableError(Exception): 20 | """ 21 | Error resulting from the attempted use of a table that doesn't exist. 22 | """ 23 | 24 | 25 | class ReferenceNotSavedError(Exception): 26 | """ 27 | Error resulting from the attempted use of an object as a reference that hasn't been 28 | saved yet. 29 | """ 30 | 31 | 32 | class CannotRefreshError(Exception): 33 | """ 34 | Error resulting from the attempted refreshing of an object that hasn't been 35 | saved yet. 36 | """ 37 | 38 | 39 | class InvalidRelationshipError(Exception): 40 | """ 41 | Error resulting from the misspecification of a relationship dictionary. 42 | """ 43 | 44 | 45 | class DBObjectSaveError(Exception): 46 | """ 47 | Error saving a DBObject. 48 | """ 49 | -------------------------------------------------------------------------------- /twistar/registry.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module handling global registration of variables and classes. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from twisted.python import reflect 7 | 8 | from twistar.exceptions import ClassNotRegisteredError 9 | 10 | 11 | class Registry(object): 12 | """ 13 | A data store containing mostly class variables that act as constants. 14 | 15 | @cvar DBPOOL: This should be set to the C{twisted.enterprise.dbapi.ConnectionPool} to 16 | use for all database interaction. 17 | """ 18 | SCHEMAS = {} 19 | REGISTRATION = {} 20 | IMPL = None 21 | DBPOOL = None 22 | 23 | 24 | @classmethod 25 | def register(_, *klasses): 26 | """ 27 | Register some number of classes in the registy. This is necessary so that when objects 28 | are created on the fly (specifically, as a result of relationship C{get}s) the package 29 | knows how to find them. 30 | 31 | @param klasses: Any number of parameters, each of which is a class. 32 | """ 33 | for klass in klasses: 34 | Registry.REGISTRATION[klass.__name__] = klass 35 | 36 | 37 | @classmethod 38 | def getClass(klass, name): 39 | """ 40 | Get a registered class by the given name. 41 | """ 42 | if name not in Registry.REGISTRATION: 43 | raise ClassNotRegisteredError("You never registered the class named %s" % name) 44 | return Registry.REGISTRATION[name] 45 | 46 | 47 | @classmethod 48 | def getDBAPIClass(klass, name): 49 | """ 50 | Per U{http://www.python.org/dev/peps/pep-0249/} each DBAPI driver must implement it's 51 | own Date/Time/Timestamp/etc classes. This method provides a generalized way to get them 52 | from whatever DB driver is being used. 53 | """ 54 | driver = Registry.DBPOOL.dbapi.__name__ 55 | path = "%s.%s" % (driver, name) 56 | return reflect.namedAny(path) 57 | 58 | 59 | @classmethod 60 | def getConfig(klass): 61 | """ 62 | Get the current DB config object being used for DB interaction. This is one of the classes 63 | that extends L{base.InteractionBase}. 64 | """ 65 | if Registry.IMPL is not None: 66 | return Registry.IMPL 67 | 68 | if Registry.DBPOOL is None: 69 | msg = "You must set Registry.DBPOOL to a adbapi.ConnectionPool before calling this method." 70 | raise RuntimeError(msg) 71 | dbapi = Registry.DBPOOL.dbapi 72 | if dbapi.__name__ == "MySQLdb": 73 | from twistar.dbconfig.mysql import MySQLDBConfig 74 | Registry.IMPL = MySQLDBConfig() 75 | elif dbapi.__name__ == "sqlite3": 76 | from twistar.dbconfig.sqlite import SQLiteDBConfig 77 | Registry.IMPL = SQLiteDBConfig() 78 | elif dbapi.__name__ == "psycopg2": 79 | from twistar.dbconfig.postgres import PostgreSQLDBConfig 80 | Registry.IMPL = PostgreSQLDBConfig() 81 | elif dbapi.__name__ == "pyodbc": 82 | from twistar.dbconfig.pyodbc import PyODBCDBConfig 83 | Registry.IMPL = PyODBCDBConfig() 84 | else: 85 | raise NotImplementedError("twisteddb does not support the %s driver" % dbapi.__name__) 86 | 87 | return Registry.IMPL 88 | -------------------------------------------------------------------------------- /twistar/relationships.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module descripting different types of object relationships. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from twisted.internet import defer 7 | 8 | from BermiInflector.Inflector import Inflector 9 | 10 | from twistar.registry import Registry 11 | from twistar.utils import createInstances, joinWheres 12 | from twistar.exceptions import ReferenceNotSavedError 13 | 14 | 15 | class Relationship(object): 16 | """ 17 | Base class that all specific relationship type classes extend. 18 | 19 | @see: L{HABTM}, L{HasOne}, L{HasMany}, L{BelongsTo} 20 | """ 21 | 22 | def __init__(self, inst, propname, givenargs): 23 | """ 24 | Constructor. 25 | 26 | @param inst: The L{DBObject} instance. 27 | 28 | @param propname: The property name in the L{DBObject} instance that 29 | results in this class being created. 30 | 31 | @param givenargs: Any arguments given (through the use of a C{dict} 32 | in the class variable in L{DBObject} rather than a string to describe 33 | the relationship). The given args can include, for all relationships, 34 | a C{class_name}. Depending on the relationship, C{association_foreign_key} 35 | and C{foreign_key} might also be used. 36 | """ 37 | self.infl = Inflector() 38 | self.inst = inst 39 | self.dbconfig = Registry.getConfig() 40 | 41 | self.args = { 42 | 'class_name': propname, 43 | 'association_foreign_key': self.infl.foreignKey(self.infl.singularize(propname)), 44 | 'foreign_key': self.infl.foreignKey(self.inst.__class__.__name__), 45 | 'polymorphic': False 46 | } 47 | self.args.update(givenargs) 48 | 49 | otherklassname = self.infl.classify(self.args['class_name']) 50 | if not self.args['polymorphic']: 51 | self.otherklass = Registry.getClass(otherklassname) 52 | self.othername = self.args['association_foreign_key'] 53 | self.thisclass = self.inst.__class__ 54 | self.thisname = self.args['foreign_key'] 55 | 56 | 57 | class BelongsTo(Relationship): 58 | """ 59 | Class representing a belongs-to relationship. 60 | """ 61 | 62 | def get(self): 63 | """ 64 | Get the object that belong to the caller. 65 | 66 | @return: A C{Deferred} with a callback value of either the matching class or 67 | None (if not set). 68 | """ 69 | def get_polymorphic(row): 70 | kid = getattr(row, "%s_id" % self.args['class_name']) 71 | kname = getattr(row, "%s_type" % self.args['class_name']) 72 | return Registry.getClass(kname).find(kid) 73 | 74 | if self.args['polymorphic']: 75 | return self.inst.find(where=["id = ?", self.inst.id], limit=1).addCallback(get_polymorphic) 76 | 77 | return self.otherklass.find(where=["id = ?", getattr(self.inst, self.othername)], limit=1) 78 | 79 | 80 | def set(self, other): 81 | """ 82 | Set the object that belongs to the caller. 83 | 84 | @return: A C{Deferred} with a callback value of the caller. 85 | """ 86 | if self.args['polymorphic']: 87 | setattr(self.inst, "%s_type" % self.args['class_name'], other.__class__.__name__) 88 | setattr(self.inst, self.othername, other.id) 89 | return self.inst.save() 90 | 91 | 92 | def clear(self): 93 | """ 94 | Remove the relationship linking the object that belongs to the caller. 95 | 96 | @return: A C{Deferred} with a callback value of the caller. 97 | """ 98 | setattr(self.inst, self.othername, None) 99 | return self.inst.save() 100 | 101 | 102 | 103 | class HasMany(Relationship): 104 | """ 105 | A class representing the has many relationship. 106 | """ 107 | 108 | def get(self, **kwargs): 109 | """ 110 | Get the objects that caller has. 111 | 112 | @param kwargs: These could include C{limit}, C{orderby}, or any others included in 113 | C{DBObject.find}. If a C{where} parameter is included, the conditions will 114 | be added to the ones already imposed by default in this method. 115 | 116 | @return: A C{Deferred} with a callback value of a list of objects. 117 | """ 118 | kwargs = self._generateGetArgs(kwargs) 119 | return self.otherklass.find(**kwargs) 120 | 121 | 122 | def count(self, **kwargs): 123 | """ 124 | Get the number of objects that caller has. 125 | 126 | @param kwargs: These could include C{limit}, C{orderby}, or any others included in 127 | C{DBObject.find}. If a C{where} parameter is included, the conditions will 128 | be added to the ones already imposed by default in this method. 129 | 130 | @return: A C{Deferred} with the number of objects. 131 | """ 132 | kwargs = self._generateGetArgs(kwargs) 133 | return self.otherklass.count(**kwargs) 134 | 135 | 136 | def _generateGetArgs(self, kwargs): 137 | if 'as' in self.args: 138 | w = "%s_id = ? AND %s_type = ?" % (self.args['as'], self.args['as']) 139 | where = [w, self.inst.id, self.thisclass.__name__] 140 | else: 141 | where = ["%s = ?" % self.thisname, self.inst.id] 142 | 143 | if 'where' in kwargs: 144 | kwargs['where'] = joinWheres(where, kwargs['where']) 145 | else: 146 | kwargs['where'] = where 147 | 148 | return kwargs 149 | 150 | 151 | def _set_polymorphic(self, others): 152 | ds = [] 153 | for other in others: 154 | if other.id is None: 155 | msg = "You must save all other instances before defining a relationship" 156 | raise ReferenceNotSavedError(msg) 157 | setattr(other, "%s_id" % self.args['as'], self.inst.id) 158 | setattr(other, "%s_type" % self.args['as'], self.thisclass.__name__) 159 | ds.append(other.save()) 160 | return defer.DeferredList(ds) 161 | 162 | 163 | def _update(self, _, others): 164 | tablename = self.otherklass.tablename() 165 | args = {self.thisname: self.inst.id} 166 | ids = [] 167 | for other in others: 168 | if other.id is None: 169 | msg = "You must save all other instances before defining a relationship" 170 | raise ReferenceNotSavedError(msg) 171 | ids.append(str(other.id)) 172 | where = ["id IN (%s)" % ",".join(ids)] 173 | return self.dbconfig.update(tablename, args, where) 174 | 175 | 176 | def set(self, others): 177 | """ 178 | Set the objects that caller has. 179 | 180 | @return: A C{Deferred}. 181 | """ 182 | if 'as' in self.args: 183 | return self._set_polymorphic(others) 184 | 185 | tablename = self.otherklass.tablename() 186 | args = {self.thisname: None} 187 | where = ["%s = ?" % self.thisname, self.inst.id] 188 | d = self.dbconfig.update(tablename, args, where) 189 | if len(others) > 0: 190 | d.addCallback(self._update, others) 191 | return d 192 | 193 | 194 | def clear(self): 195 | """ 196 | Clear the list of all of the objects that this one has. 197 | """ 198 | return self.set([]) 199 | 200 | 201 | class HasOne(Relationship): 202 | """ 203 | A class representing the has one relationship. 204 | """ 205 | 206 | def get(self): 207 | """ 208 | Get the object that caller has. 209 | 210 | @return: A C{Deferred} with a callback value of the object this one has (or c{None}). 211 | """ 212 | return self.otherklass.find(where=["%s = ?" % self.thisname, self.inst.id], limit=1) 213 | 214 | 215 | def set(self, other): 216 | """ 217 | Set the object that caller has. 218 | 219 | @return: A C{Deferred}. 220 | """ 221 | tablename = self.otherklass.tablename() 222 | args = {self.thisname: self.inst.id} 223 | where = ["id = ?", other.id] 224 | return self.dbconfig.update(tablename, args, where) 225 | 226 | 227 | class HABTM(Relationship): 228 | """ 229 | A class representing the "has and belongs to many" relationship. One additional argument 230 | this class uses in the L{Relationship.__init__} argument list is C{join_table}. 231 | """ 232 | 233 | def tablename(self): 234 | """ 235 | Get the tablename (specified either in the C{join_table} relationship property 236 | or by calculating the tablename). If not specified, the table name is calculated 237 | by sorting the table name versions of the two class names and joining them with a '_'). 238 | For instance, given the classes C{Teacher} and C{Student}, the resulting table name would 239 | be C{student_teacher}. 240 | """ 241 | # if specified by user 242 | if 'join_table' in self.args: 243 | return self.args['join_table'] 244 | 245 | # otherwise, create and cache 246 | if not hasattr(self, '_tablename'): 247 | thistable = self.infl.tableize(self.thisclass.__name__) 248 | othertable = self.infl.tableize(self.otherklass.__name__) 249 | tables = [thistable, othertable] 250 | tables.sort() 251 | self._tablename = "_".join(tables) 252 | return self._tablename 253 | 254 | 255 | def get(self, **kwargs): 256 | """ 257 | Get the objects that caller has. 258 | 259 | @param kwargs: These could include C{limit}, C{orderby}, or any others included in 260 | C{InteractionBase.select}. If a C{where} parameter is included, the conditions will 261 | be added to the ones already imposed by default in this method. The argument 262 | C{join_where} will be applied to the join table, if provided. 263 | 264 | @return: A C{Deferred} with a callback value of a list of objects. 265 | """ 266 | def _get(rows): 267 | if len(rows) == 0: 268 | return defer.succeed([]) 269 | ids = [str(row[self.othername]) for row in rows] 270 | where = ["id IN (%s)" % ",".join(ids)] 271 | if 'where' in kwargs: 272 | kwargs['where'] = joinWheres(where, kwargs['where']) 273 | else: 274 | kwargs['where'] = where 275 | d = self.dbconfig.select(self.otherklass.tablename(), **kwargs) 276 | return d.addCallback(createInstances, self.otherklass) 277 | 278 | tablename = self.tablename() 279 | where = ["%s = ?" % self.thisname, self.inst.id] 280 | if 'join_where' in kwargs: 281 | where = joinWheres(where, kwargs.pop('join_where')) 282 | return self.dbconfig.select(tablename, where=where).addCallback(_get) 283 | 284 | 285 | def count(self, **kwargs): 286 | """ 287 | Get the number of objects that caller has. 288 | 289 | @param kwargs: These could include C{limit}, C{orderby}, or any others included in 290 | C{InteractionBase.select}. If a C{where} parameter is included, the conditions will 291 | be added to the ones already imposed by default in this method. 292 | 293 | @return: A C{Deferred} with the number of objects. 294 | """ 295 | def _get(rows): 296 | if len(rows) == 0: 297 | return defer.succeed(0) 298 | if 'where' not in kwargs: 299 | return defer.succeed(len(rows)) 300 | ids = [str(row[self.othername]) for row in rows] 301 | where = ["id IN (%s)" % ",".join(ids)] 302 | if 'where' in kwargs: 303 | where = joinWheres(where, kwargs['where']) 304 | return self.dbconfig.count(self.otherklass.tablename(), where=where) 305 | 306 | tablename = self.tablename() 307 | where = ["%s = ?" % self.thisname, self.inst.id] 308 | return self.dbconfig.select(tablename, where=where).addCallback(_get) 309 | 310 | 311 | def _set(self, _, others): 312 | args = [] 313 | for other in others: 314 | if other.id is None: 315 | msg = "You must save all other instances before defining a relationship" 316 | raise ReferenceNotSavedError(msg) 317 | args.append({self.thisname: self.inst.id, self.othername: other.id}) 318 | return self.dbconfig.insertMany(self.tablename(), args) 319 | 320 | 321 | def set(self, others): 322 | """ 323 | Set the objects that caller has. 324 | 325 | @return: A C{Deferred}. 326 | """ 327 | where = ["%s = ?" % self.thisname, self.inst.id] 328 | d = self.dbconfig.delete(self.tablename(), where=where) 329 | if len(others) > 0: 330 | d.addCallback(self._set, others) 331 | return d 332 | 333 | 334 | def clear(self): 335 | """ 336 | Clear the list of all of the objects that this one has. 337 | """ 338 | return self.set([]) 339 | 340 | 341 | Relationship.TYPES = {'HASMANY': HasMany, 'HASONE': HasOne, 'BELONGSTO': BelongsTo, 'HABTM': HABTM} 342 | -------------------------------------------------------------------------------- /twistar/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmuller/twistar/1eb46ff2577473e0a26932ee57473e26203a3db2/twistar/tests/__init__.py -------------------------------------------------------------------------------- /twistar/tests/mysql_config.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twisted.enterprise import adbapi 3 | 4 | from twistar.registry import Registry 5 | 6 | CONNECTION = Registry.DBPOOL = adbapi.ConnectionPool('MySQLdb', user="root", passwd="", host="localhost", db="twistar") 7 | 8 | 9 | def initDB(testKlass): 10 | def runInitTxn(txn): 11 | txn.execute("""CREATE TABLE users (id INT AUTO_INCREMENT, 12 | first_name VARCHAR(255), last_name VARCHAR(255), age INT, dob DATE, PRIMARY KEY (id))""") 13 | txn.execute("""CREATE TABLE avatars (id INT AUTO_INCREMENT, name VARCHAR(255), 14 | color VARCHAR(255), user_id INT, PRIMARY KEY (id))""") 15 | txn.execute("""CREATE TABLE pictures (id INT AUTO_INCREMENT, name VARCHAR(255), 16 | size INT, user_id INT, PRIMARY KEY (id))""") 17 | txn.execute("""CREATE TABLE comments (id INT AUTO_INCREMENT, subject VARCHAR(255), 18 | body TEXT, user_id INT, PRIMARY KEY (id))""") 19 | txn.execute("""CREATE TABLE favorite_colors (id INT AUTO_INCREMENT, name VARCHAR(255), PRIMARY KEY (id))""") 20 | txn.execute("""CREATE TABLE favorite_colors_users (favorite_color_id INT, user_id INT, palette_id INT)""") 21 | txn.execute("""CREATE TABLE coltests (id INT AUTO_INCREMENT, `select` VARCHAR(255), `where` VARCHAR(255), PRIMARY KEY (id))""") 22 | 23 | txn.execute("""CREATE TABLE boys (id INT AUTO_INCREMENT, `name` VARCHAR(255), PRIMARY KEY (id))""") 24 | txn.execute("""CREATE TABLE girls (id INT AUTO_INCREMENT, `name` VARCHAR(255), PRIMARY KEY (id))""") 25 | txn.execute("""CREATE TABLE nicknames (id INT AUTO_INCREMENT, `value` VARCHAR(255), `nicknameable_id` INT, 26 | `nicknameable_type` VARCHAR(255), PRIMARY KEY(id))""") 27 | txn.execute("""CREATE TABLE blogposts (id INT AUTO_INCREMENT, 28 | title VARCHAR(255), text VARCHAR(255), PRIMARY KEY (id))""") 29 | txn.execute("""CREATE TABLE categories (id INT AUTO_INCREMENT, 30 | name VARCHAR(255), PRIMARY KEY (id))""") 31 | txn.execute("""CREATE TABLE posts_categories (category_id INT, blogpost_id INT)""") 32 | txn.execute("""CREATE TABLE transactions (id INT AUTO_INCREMENT, name VARCHAR(255), PRIMARY KEY (id), UNIQUE(name))""") 33 | 34 | return CONNECTION.runInteraction(runInitTxn) 35 | 36 | 37 | def tearDownDB(self): 38 | def runTearDownDB(txn): 39 | txn.execute("DROP TABLE users") 40 | txn.execute("DROP TABLE avatars") 41 | txn.execute("DROP TABLE pictures") 42 | txn.execute("DROP TABLE comments") 43 | txn.execute("DROP TABLE favorite_colors") 44 | txn.execute("DROP TABLE favorite_colors_users") 45 | txn.execute("DROP TABLE coltests") 46 | txn.execute("DROP TABLE boys") 47 | txn.execute("DROP TABLE girls") 48 | txn.execute("DROP TABLE nicknames") 49 | txn.execute("DROP TABLE blogposts") 50 | txn.execute("DROP TABLE categories") 51 | txn.execute("DROP TABLE posts_categories") 52 | txn.execute("DROP TABLE transactions") 53 | return CONNECTION.runInteraction(runTearDownDB) 54 | -------------------------------------------------------------------------------- /twistar/tests/postgres_config.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twisted.enterprise import adbapi 3 | 4 | from twistar.registry import Registry 5 | 6 | CONNECTION = Registry.DBPOOL = adbapi.ConnectionPool('psycopg2', "dbname=twistar") 7 | 8 | 9 | def initDB(testKlass): 10 | def runInitTxn(txn): 11 | txn.execute("""CREATE TABLE users (id SERIAL PRIMARY KEY, 12 | first_name VARCHAR(255), last_name VARCHAR(255), age INT, dob DATE)""") 13 | txn.execute("""CREATE TABLE avatars (id SERIAL PRIMARY KEY, name VARCHAR(255), 14 | color VARCHAR(255), user_id INT)""") 15 | txn.execute("""CREATE TABLE pictures (id SERIAL PRIMARY KEY, name VARCHAR(255), 16 | size INT, user_id INT)""") 17 | txn.execute("""CREATE TABLE comments (id SERIAL PRIMARY KEY, subject VARCHAR(255), 18 | body TEXT, user_id INT)""") 19 | txn.execute("""CREATE TABLE favorite_colors (id SERIAL PRIMARY KEY, name VARCHAR(255))""") 20 | txn.execute("""CREATE TABLE favorite_colors_users (favorite_color_id INT, user_id INT, palette_id INT)""") 21 | txn.execute("""CREATE TABLE coltests (id SERIAL PRIMARY KEY, "select" VARCHAR(255), "where" VARCHAR(255))""") 22 | 23 | txn.execute("""CREATE TABLE boys (id SERIAL PRIMARY KEY, "name" VARCHAR(255))""") 24 | txn.execute("""CREATE TABLE girls (id SERIAL PRIMARY KEY, "name" VARCHAR(255))""") 25 | txn.execute("""CREATE TABLE nicknames (id SERIAL PRIMARY KEY, "value" VARCHAR(255), "nicknameable_id" INT, 26 | "nicknameable_type" VARCHAR(255))""") 27 | txn.execute("""CREATE TABLE blogposts (id SERIAL PRIMARY KEY, 28 | title VARCHAR(255), text VARCHAR(255))""") 29 | txn.execute("""CREATE TABLE categories (id SERIAL PRIMARY KEY, 30 | name VARCHAR(255))""") 31 | txn.execute("""CREATE TABLE posts_categories (category_id INT, blogpost_id INT)""") 32 | txn.execute("""CREATE TABLE transactions (id SERIAL PRIMARY KEY, name VARCHAR(255) UNIQUE)""") 33 | 34 | return CONNECTION.runInteraction(runInitTxn) 35 | 36 | 37 | def tearDownDB(self): 38 | def runTearDownDB(txn): 39 | txn.execute("DROP SEQUENCE users_id_seq CASCADE") 40 | txn.execute("DROP TABLE users") 41 | 42 | txn.execute("DROP SEQUENCE avatars_id_seq CASCADE") 43 | txn.execute("DROP TABLE avatars") 44 | 45 | txn.execute("DROP SEQUENCE pictures_id_seq CASCADE") 46 | txn.execute("DROP TABLE pictures") 47 | 48 | txn.execute("DROP SEQUENCE comments_id_seq CASCADE") 49 | txn.execute("DROP TABLE comments") 50 | 51 | txn.execute("DROP SEQUENCE favorite_colors_id_seq CASCADE") 52 | txn.execute("DROP TABLE favorite_colors") 53 | 54 | txn.execute("DROP TABLE favorite_colors_users") 55 | 56 | txn.execute("DROP SEQUENCE coltests_id_seq CASCADE") 57 | txn.execute("DROP TABLE coltests") 58 | 59 | txn.execute("DROP SEQUENCE boys_id_seq CASCADE") 60 | txn.execute("DROP TABLE boys") 61 | 62 | txn.execute("DROP SEQUENCE girls_id_seq CASCADE") 63 | txn.execute("DROP TABLE girls") 64 | 65 | txn.execute("DROP SEQUENCE nicknames_id_seq CASCADE") 66 | txn.execute("DROP TABLE nicknames") 67 | 68 | txn.execute("DROP SEQUENCE blogposts_id_seq CASCADE") 69 | txn.execute("DROP TABLE blogposts") 70 | 71 | txn.execute("DROP SEQUENCE categories_id_seq CASCADE") 72 | txn.execute("DROP TABLE categories") 73 | 74 | txn.execute("DROP TABLE posts_categories") 75 | 76 | txn.execute("DROP SEQUENCE transactions_id_seq CASCADE") 77 | txn.execute("DROP TABLE transactions") 78 | 79 | return CONNECTION.runInteraction(runTearDownDB) 80 | -------------------------------------------------------------------------------- /twistar/tests/sqlite_config.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twisted.enterprise import adbapi 3 | from twisted.internet import defer 4 | 5 | from twistar.registry import Registry 6 | 7 | 8 | def initDB(testKlass): 9 | location = testKlass.mktemp() 10 | Registry.DBPOOL = adbapi.ConnectionPool('sqlite3', location, check_same_thread=False) 11 | 12 | def runInitTxn(txn): 13 | txn.execute("""CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, 14 | first_name TEXT, last_name TEXT, age INTEGER, dob DATE)""") 15 | txn.execute("""CREATE TABLE avatars (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, 16 | color TEXT, user_id INTEGER)""") 17 | txn.execute("""CREATE TABLE pictures (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, 18 | size INTEGER, user_id INTEGER)""") 19 | txn.execute("""CREATE TABLE comments (id INTEGER PRIMARY KEY AUTOINCREMENT, subject TEXT, 20 | body TEXT, user_id INTEGER)""") 21 | txn.execute("""CREATE TABLE favorite_colors (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)""") 22 | txn.execute("""CREATE TABLE favorite_colors_users (favorite_color_id INTEGER, user_id INTEGER, palette_id INTEGER)""") 23 | txn.execute("""CREATE TABLE coltests (id INTEGER PRIMARY KEY AUTOINCREMENT, `select` TEXT, `where` TEXT)""") 24 | 25 | txn.execute("""CREATE TABLE boys (id INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)""") 26 | txn.execute("""CREATE TABLE girls (id INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)""") 27 | txn.execute("""CREATE TABLE nicknames (id INTEGER PRIMARY KEY AUTOINCREMENT, `value` TEXT, `nicknameable_id` INTEGER, 28 | `nicknameable_type` TEXT)""") 29 | txn.execute("""CREATE TABLE blogposts (id INTEGER PRIMARY KEY AUTOINCREMENT, 30 | title TEXT, text TEXT)""") 31 | txn.execute("""CREATE TABLE categories (id INTEGER PRIMARY KEY AUTOINCREMENT, 32 | name TEXT)""") 33 | txn.execute("""CREATE TABLE posts_categories (category_id INTEGER, blogpost_id INTEGER)""") 34 | txn.execute("""CREATE TABLE transactions (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, UNIQUE (name))""") 35 | return Registry.DBPOOL.runInteraction(runInitTxn) 36 | 37 | 38 | def tearDownDB(self): 39 | return defer.succeed(True) 40 | -------------------------------------------------------------------------------- /twistar/tests/test_dbconfig.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twisted.trial import unittest 3 | from twisted.internet.defer import inlineCallbacks 4 | 5 | from twistar.registry import Registry 6 | from twistar.dbconfig.base import InteractionBase 7 | 8 | from .utils import User, Picture, Avatar, initDB, tearDownDB, Coltest 9 | from six.moves import range 10 | 11 | 12 | class DBConfigTest(unittest.TestCase): 13 | @inlineCallbacks 14 | def setUp(self): 15 | yield initDB(self) 16 | self.user = yield User(first_name="First", last_name="Last", age=10).save() 17 | self.avatar = yield Avatar(name="an avatar name", user_id=self.user.id).save() 18 | self.picture = yield Picture(name="a pic", size=10, user_id=self.user.id).save() 19 | self.dbconfig = Registry.getConfig() 20 | 21 | 22 | @inlineCallbacks 23 | def tearDown(self): 24 | yield tearDownDB(self) 25 | 26 | 27 | @inlineCallbacks 28 | def test_select(self): 29 | # make a fake user 30 | user = yield User(first_name="Another First").save() 31 | tablename = User.tablename() 32 | 33 | where = ['first_name = ?', "First"] 34 | result = yield self.dbconfig.select(tablename, where=where, limit=1, orderby="first_name ASC") 35 | self.assertTrue(result is not None) 36 | self.assertEqual(result['id'], self.user.id) 37 | 38 | result = yield self.dbconfig.select(tablename, limit=100, orderby="first_name ASC") 39 | self.assertEqual(len(result), 2) 40 | self.assertTrue(result[0]['id'] == user.id and result[1]['id'] == self.user.id) 41 | 42 | 43 | @inlineCallbacks 44 | def test_select_id(self): 45 | tablename = User.tablename() 46 | 47 | result = yield self.dbconfig.select(tablename, self.user.id, where=None, limit=1, orderby="first_name ASC") 48 | self.assertTrue(result is not None) 49 | 50 | where = ['first_name = ?', "DNE"] 51 | result = yield self.dbconfig.select(tablename, self.user.id, where=where, limit=1, orderby="first_name ASC") 52 | self.assertTrue(result is None) 53 | 54 | 55 | @inlineCallbacks 56 | def test_delete(self): 57 | tablename = User.tablename() 58 | 59 | yield User(first_name="Another First").save() 60 | yield self.dbconfig.delete(tablename, ['first_name like ?', "%nother Fir%"]) 61 | 62 | result = yield self.dbconfig.select(tablename) 63 | self.assertEqual(len(result), 1) 64 | self.assertTrue(result[0]['id'] == self.user.id) 65 | 66 | 67 | @inlineCallbacks 68 | def test_update(self): 69 | tablename = User.tablename() 70 | user = yield User(first_name="Another First").save() 71 | 72 | args = {'first_name': "test", "last_name": "foo", "age": 91} 73 | yield self.dbconfig.update(tablename, args, ['id = ?', user.id]) 74 | yield user.refresh() 75 | for key, value in args.items(): 76 | self.assertEqual(value, getattr(user, key)) 77 | 78 | 79 | @inlineCallbacks 80 | def test_insert(self): 81 | tablename = User.tablename() 82 | args = {'first_name': "test", "last_name": "foo", "age": 91} 83 | id = yield self.dbconfig.insert(tablename, args) 84 | 85 | where = ['first_name = ? AND last_name = ? AND age = ?'] 86 | where = where + ["test", "foo", 91] 87 | users = yield User.find(where=where) 88 | 89 | self.assertEqual(len(users), 1) 90 | self.assertEqual(users[0].id, id) 91 | for key, value in args.items(): 92 | self.assertEqual(value, getattr(users[0], key)) 93 | 94 | 95 | def test_insertWithTx(self): 96 | def run(txn): 97 | tablename = User.tablename() 98 | args = {'first_name': "test", "last_name": "foo", "age": 91} 99 | objid = self.dbconfig.insert(tablename, args, txn) 100 | users = self.dbconfig._doselect(txn, "select * from %s" % User.tablename(), [], User.tablename()) 101 | self.assertEqual(len(users), 2) 102 | self.assertEqual(users[1]['id'], objid) 103 | for key, value in args.items(): 104 | self.assertEqual(value, users[1][key]) 105 | return self.dbconfig.runInteraction(run) 106 | 107 | 108 | @inlineCallbacks 109 | def test_insert_many(self): 110 | tablename = User.tablename() 111 | 112 | args = [] 113 | for counter in range(10): 114 | args.append({'first_name': "test_insert_many", "last_name": "foo", "age": counter}) 115 | yield self.dbconfig.insertMany(tablename, args) 116 | 117 | users = yield User.find(where=['first_name = ?', "test_insert_many"], orderby="age ASC") 118 | 119 | for counter in range(10): 120 | for key, value in args[counter].items(): 121 | self.assertEqual(value, getattr(users[counter], key)) 122 | 123 | 124 | @inlineCallbacks 125 | def test_insert_obj(self): 126 | args = {'first_name': "test_insert_obj", "last_name": "foo", "age": 91} 127 | user = User(**args) 128 | 129 | saved = yield self.dbconfig.insertObj(user) 130 | user = yield User.find(where=['first_name = ?', "test_insert_obj"], limit=1) 131 | # ensure that id was set on save 132 | self.assertEqual(saved.id, user.id) 133 | # and all values are still the same 134 | self.assertEqual(saved, user) 135 | 136 | for key, value in args.items(): 137 | self.assertEqual(value, getattr(user, key)) 138 | 139 | 140 | @inlineCallbacks 141 | def test_update_obj(self): 142 | args = {'first_name': "test_insert_obj", "last_name": "foo", "age": 91} 143 | user = yield User(**args).save() 144 | 145 | args = {'first_name': "test_insert_obj_foo", "last_name": "bar", "age": 191} 146 | for key, value in args.items(): 147 | setattr(user, key, value) 148 | 149 | yield self.dbconfig.updateObj(user) 150 | user = yield User.find(user.id) 151 | 152 | for key, value in args.items(): 153 | self.assertEqual(value, getattr(user, key)) 154 | 155 | 156 | @inlineCallbacks 157 | def test_colname_escaping(self): 158 | args = {'select': "some text", 'where': "other text"} 159 | coltest = Coltest(**args) 160 | yield self.dbconfig.insertObj(coltest) 161 | 162 | args = {'select': "other text", 'where': "some text"} 163 | for key, value in args.items(): 164 | setattr(coltest, key, value) 165 | yield self.dbconfig.updateObj(coltest) 166 | 167 | tablename = Coltest.tablename() 168 | colnames = self.dbconfig.escapeColNames(["select"]) 169 | ctest = yield self.dbconfig.select(tablename, where=['%s = ?' % colnames[0], args['select']], limit=1) 170 | 171 | for key, value in args.items(): 172 | self.assertEqual(value, ctest[key]) 173 | 174 | 175 | def test_unicode_logging(self): 176 | InteractionBase.LOG = True 177 | 178 | ustr = u'\N{SNOWMAN}' 179 | InteractionBase().log(ustr, [ustr], {ustr: ustr}) 180 | 181 | ustr = '\xc3\xa8' 182 | InteractionBase().log(ustr, [ustr], {ustr: ustr}) 183 | InteractionBase().log(ustr, [], {ustr: ustr}) 184 | 185 | InteractionBase.LOG = False 186 | -------------------------------------------------------------------------------- /twistar/tests/test_dbobject.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twisted.trial import unittest 3 | from twisted.internet.defer import inlineCallbacks 4 | 5 | from twistar.exceptions import ImaginaryTableError 6 | from twistar.registry import Registry 7 | 8 | from .utils import User, Avatar, Picture, tearDownDB, initDB, FakeObject, DBObject 9 | from six.moves import range 10 | 11 | 12 | class DBObjectTest(unittest.TestCase): 13 | @inlineCallbacks 14 | def setUp(self): 15 | yield initDB(self) 16 | self.user = yield User(first_name="First", last_name="Last", age=10).save() 17 | self.avatar = yield Avatar(name="an avatar name", user_id=self.user.id).save() 18 | self.picture = yield Picture(name="a pic", size=10, user_id=self.user.id).save() 19 | 20 | 21 | @inlineCallbacks 22 | def tearDown(self): 23 | yield tearDownDB(self) 24 | 25 | 26 | @inlineCallbacks 27 | def test_findBy(self): 28 | r = yield User.findBy(first_name="Non", last_name="Existant") 29 | self.assertEqual(r, []) 30 | 31 | r = yield User.findBy(first_name="First", last_name="Last", age=11) 32 | self.assertEqual(r, []) 33 | 34 | r = yield User.findBy(first_name="First", last_name="Last", age=10) 35 | self.assertEqual(r[0], self.user) 36 | 37 | r = yield User.findBy(first_name="First", last_name="Last") 38 | self.assertEqual(r[0], self.user) 39 | 40 | yield User(first_name="Bob").save() 41 | r = yield User.findBy() 42 | self.assertEqual(len(r), 2) 43 | 44 | yield User(first_name=None).save() 45 | r = yield User.findBy(first_name=None) 46 | self.assertEqual(len(r), 1) 47 | 48 | 49 | @inlineCallbacks 50 | def test_findOrCreate(self): 51 | # make sure we didn't create a new user 52 | r = yield User.findOrCreate(first_name="First") 53 | self.assertEqual(r.id, self.user.id) 54 | 55 | # make sure we do create a new user 56 | r = yield User.findOrCreate(first_name="First", last_name="Non") 57 | self.assertTrue(r.id != self.user.id) 58 | 59 | 60 | @inlineCallbacks 61 | def test_creation(self): 62 | # test creating blank object 63 | u = yield User().save() 64 | self.assertTrue(type(u.id) == int or type(u.id) == int) 65 | 66 | # test creating object with props that don't correspond to columns 67 | u = yield User(a_fake_column="blech").save() 68 | self.assertTrue(type(u.id) == int or type(u.id) == int) 69 | 70 | # Test table doesn't exist 71 | f = FakeObject(blah="something") 72 | self.failUnlessFailure(f.save(), ImaginaryTableError) 73 | 74 | dateklass = Registry.getDBAPIClass("Date") 75 | args = {'first_name': "a", "last_name": "b", "age": 10, "dob": dateklass(2000, 1, 1)} 76 | u = yield User(**args).save() 77 | for key, value in args.items(): 78 | self.assertEqual(getattr(u, key), value) 79 | 80 | 81 | @inlineCallbacks 82 | def test_find(self): 83 | ids = [] 84 | for _ in range(3): 85 | user = yield User(first_name="blah").save() 86 | ids.append(user.id) 87 | yield User(first_name="not blah").save() 88 | results = yield User.find(where=["first_name = ?", "blah"]) 89 | resultids = [result.id for result in results] 90 | self.assertEqual(ids, resultids) 91 | 92 | 93 | @inlineCallbacks 94 | def test_count(self): 95 | ids = [] 96 | for _ in range(3): 97 | user = yield User(first_name="blah").save() 98 | ids.append(user.id) 99 | yield User(first_name="not blah").save() 100 | results = yield User.count(where=["first_name = ?", "blah"]) 101 | self.assertEqual(3, results) 102 | 103 | 104 | @inlineCallbacks 105 | def test_all(self): 106 | ids = [self.user.id] 107 | for _ in range(3): 108 | user = yield User(first_name="blah").save() 109 | ids.append(user.id) 110 | results = yield User.all() 111 | resultids = [result.id for result in results] 112 | self.assertEqual(ids, resultids) 113 | 114 | 115 | @inlineCallbacks 116 | def test_count_all(self): 117 | ids = [self.user.id] 118 | for _ in range(3): 119 | user = yield User(first_name="blah").save() 120 | ids.append(user.id) 121 | results = yield User.count() 122 | self.assertEqual(4, results) 123 | 124 | 125 | @inlineCallbacks 126 | def test_delete(self): 127 | u = yield User().save() 128 | oldid = u.id 129 | yield u.delete() 130 | result = yield User.find(oldid) 131 | self.assertEqual(result, None) 132 | 133 | 134 | @inlineCallbacks 135 | def test_delete_all(self): 136 | users = yield User.all() 137 | ids = [user.id for user in users] 138 | for _ in range(3): 139 | yield User(first_name="blah").save() 140 | yield User.deleteAll(["first_name = ?", "blah"]) 141 | users = yield User.all() 142 | resultids = [user.id for user in users] 143 | self.assertEqual(resultids, ids) 144 | 145 | 146 | @inlineCallbacks 147 | def test_update(self): 148 | args = {'first_name': "a", "last_name": "b", "age": 10} 149 | u = yield User(**args).save() 150 | 151 | args = {'first_name': "b", "last_name": "a", "age": 100} 152 | for key, value in args.items(): 153 | setattr(u, key, value) 154 | yield u.save() 155 | 156 | u = yield User.find(u.id) 157 | for key, value in args.items(): 158 | self.assertEqual(getattr(u, key), value) 159 | 160 | 161 | @inlineCallbacks 162 | def test_refresh(self): 163 | args = {'first_name': "a", "last_name": "b", "age": 10} 164 | u = yield User(**args).save() 165 | 166 | # mess up the props, then refresh 167 | u.first_name = "something different" 168 | u.last_name = "another thing" 169 | yield u.refresh() 170 | 171 | for key, value in args.items(): 172 | self.assertEqual(getattr(u, key), value) 173 | 174 | 175 | @inlineCallbacks 176 | def test_validation(self): 177 | User.validatesPresenceOf('first_name', message='cannot be blank, fool.') 178 | User.validatesLengthOf('last_name', range=range(1, 101)) 179 | User.validatesUniquenessOf('first_name') 180 | 181 | u = User() 182 | yield u.validate() 183 | self.assertEqual(len(u.errors), 2) 184 | 185 | first = yield User(first_name="not unique", last_name="not unique").save() 186 | u = yield User(first_name="not unique", last_name="not unique").save() 187 | self.assertEqual(len(u.errors), 1) 188 | self.assertEqual(u.id, None) 189 | 190 | # make sure first can be updated 191 | yield first.save() 192 | self.assertEqual(len(first.errors), 0) 193 | User.clearValidations() 194 | 195 | 196 | @inlineCallbacks 197 | def test_validation_function(self): 198 | def adult(user): 199 | if user.age < 18: 200 | user.errors.add('age', "must be over 18.") 201 | User.addValidator(adult) 202 | 203 | u = User(age=10) 204 | valid = yield u.isValid() 205 | self.assertEqual(valid, False) 206 | yield u.save() 207 | self.assertEqual(len(u.errors), 1) 208 | self.assertEqual(len(u.errors.errorsFor('age')), 1) 209 | self.assertEqual(len(u.errors.errorsFor('first_name')), 0) 210 | User.clearValidations() 211 | 212 | u = User(age=10) 213 | valid = yield u.isValid() 214 | self.assertEqual(valid, True) 215 | User.clearValidations() 216 | 217 | 218 | @inlineCallbacks 219 | def test_afterInit(self): 220 | def afterInit(user): 221 | user.blah = "foobar" 222 | User.afterInit = afterInit 223 | u = yield User.find(limit=1) 224 | self.assertTrue(hasattr(u, 'blah')) 225 | self.assertEqual(u.blah, 'foobar') 226 | 227 | # restore user's afterInit 228 | User.afterInit = DBObject.afterInit 229 | 230 | 231 | @inlineCallbacks 232 | def test_beforeDelete(self): 233 | User.beforeDelete = lambda user: False 234 | u = yield User().save() 235 | oldid = u.id 236 | yield u.delete() 237 | result = yield User.find(oldid) 238 | self.assertEqual(result, u) 239 | 240 | User.beforeDelete = lambda user: True 241 | yield u.delete() 242 | result = yield User.find(oldid) 243 | self.assertEqual(result, None) 244 | 245 | # restore user's beforeDelete 246 | User.beforeDelete = DBObject.beforeDelete 247 | 248 | @inlineCallbacks 249 | def test_loadRelations(self): 250 | user = yield User.find(limit=1) 251 | all = yield user.loadRelations() 252 | 253 | pictures = yield user.pictures.get() 254 | self.assertEqual(pictures, all['pictures']) 255 | 256 | avatar = yield user.avatar.get() 257 | self.assertEqual(avatar, all['avatar']) 258 | 259 | suball = yield user.loadRelations('pictures') 260 | self.assertTrue('avatar' not in suball) 261 | self.assertEqual(pictures, suball['pictures']) 262 | -------------------------------------------------------------------------------- /twistar/tests/test_relationships.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twisted.trial import unittest 3 | from twisted.internet.defer import inlineCallbacks 4 | 5 | from twistar.exceptions import ReferenceNotSavedError 6 | 7 | from .utils import Boy, Girl, tearDownDB, initDB, Registry, Comment, Category 8 | from .utils import User, Avatar, Picture, FavoriteColor, Nickname, Blogpost 9 | from six.moves import range 10 | 11 | 12 | class RelationshipTest(unittest.TestCase): 13 | @inlineCallbacks 14 | def setUp(self): 15 | yield initDB(self) 16 | self.user = yield User(first_name="First", last_name="Last", age=10).save() 17 | self.avatar = yield Avatar(name="an avatar name", user_id=self.user.id).save() 18 | self.picture = yield Picture(name="a pic", size=10, user_id=self.user.id).save() 19 | self.favcolor = yield FavoriteColor(name="blue").save() 20 | self.boy = yield Boy(name="Robert").save() 21 | self.girl = yield Girl(name="Susan").save() 22 | self.config = Registry.getConfig() 23 | 24 | 25 | @inlineCallbacks 26 | def tearDown(self): 27 | yield tearDownDB(self) 28 | 29 | 30 | @inlineCallbacks 31 | def test_polymorphic_get(self): 32 | bob = yield Nickname(value="Bob", nicknameable_id=self.boy.id, nicknameable_type="Boy").save() 33 | sue = yield Nickname(value="Sue", nicknameable_id=self.girl.id, nicknameable_type="Girl").save() 34 | 35 | nicknames = yield self.boy.nicknames.get() 36 | self.assertEqual(len(nicknames), 1) 37 | self.assertEqual(nicknames[0], bob) 38 | self.assertEqual(nicknames[0].value, bob.value) 39 | 40 | nicknames = yield self.girl.nicknames.get() 41 | self.assertEqual(len(nicknames), 1) 42 | self.assertEqual(nicknames[0], sue) 43 | self.assertEqual(nicknames[0].value, sue.value) 44 | 45 | boy = yield bob.nicknameable.get() 46 | self.assertEqual(boy, self.boy) 47 | 48 | girl = yield sue.nicknameable.get() 49 | self.assertEqual(girl, self.girl) 50 | 51 | 52 | @inlineCallbacks 53 | def test_polymorphic_set(self): 54 | nicknameone = yield Nickname(value="Bob").save() 55 | nicknametwo = yield Nickname(value="Bobby").save() 56 | yield self.boy.nicknames.set([nicknametwo, nicknameone]) 57 | 58 | nicknames = yield self.boy.nicknames.get() 59 | self.assertEqual(len(nicknames), 2) 60 | # since the insert is asynchronous - two may have been inserted 61 | # before one 62 | if not nicknames[0] == nicknametwo: 63 | self.assertEqual(nicknames[0], nicknameone) 64 | if not nicknames[1] == nicknameone: 65 | self.assertEqual(nicknames[1], nicknametwo) 66 | 67 | boy = yield nicknameone.nicknameable.get() 68 | self.assertEqual(boy, self.boy) 69 | 70 | nickname = yield Nickname(value="Suzzy").save() 71 | yield nickname.nicknameable.set(self.girl) 72 | nicknames = yield self.girl.nicknames.get() 73 | self.assertEqual(len(nicknames), 1) 74 | self.assertEqual(nicknames[0], nickname) 75 | self.assertEqual(nicknames[0].value, nickname.value) 76 | 77 | 78 | @inlineCallbacks 79 | def test_belongs_to(self): 80 | user = yield self.picture.user.get() 81 | self.assertEqual(user, self.user) 82 | 83 | 84 | @inlineCallbacks 85 | def test_set_belongs_to(self): 86 | user = yield User(first_name="new one").save() 87 | yield self.picture.user.set(user) 88 | self.assertEqual(user.id, self.picture.user_id) 89 | 90 | 91 | @inlineCallbacks 92 | def test_set_on_unsaved(self): 93 | yield User(first_name="new one").save() 94 | picture = Picture(name="a pic") 95 | self.assertRaises(ReferenceNotSavedError, getattr, picture, 'user') 96 | 97 | 98 | @inlineCallbacks 99 | def test_clear_belongs_to(self): 100 | picture = yield Picture(name="a pic", size=10, user_id=self.user.id).save() 101 | yield picture.user.clear() 102 | user = yield picture.user.get() 103 | self.assertEqual(user, None) 104 | yield picture.refresh() 105 | user = yield picture.user.get() 106 | self.assertEqual(user, None) 107 | 108 | 109 | @inlineCallbacks 110 | def test_has_many(self): 111 | # First, make a few pics 112 | ids = [self.picture.id] 113 | for _ in range(3): 114 | pic = yield Picture(user_id=self.user.id).save() 115 | ids.append(pic.id) 116 | 117 | pics = yield self.user.pictures.get() 118 | picids = [p.id for p in pics] 119 | self.assertEqual(ids, picids) 120 | 121 | 122 | @inlineCallbacks 123 | def test_has_many_count(self): 124 | # First, make a few pics 125 | ids = [self.picture.id] 126 | for _ in range(3): 127 | pic = yield Picture(user_id=self.user.id).save() 128 | ids.append(pic.id) 129 | 130 | totalnum = yield self.user.pictures.count() 131 | self.assertEqual(totalnum, 4) 132 | 133 | 134 | @inlineCallbacks 135 | def test_has_many_count_nocache(self): 136 | # First, count comments 137 | totalnum = yield self.user.comments.count() 138 | self.assertEqual(totalnum, 0) 139 | 140 | for _ in range(3): 141 | yield Comment(user_id=self.user.id).save() 142 | 143 | totalnum = yield self.user.comments.count() 144 | self.assertEqual(totalnum, 3) 145 | 146 | 147 | @inlineCallbacks 148 | def test_has_many_get_with_args(self): 149 | # First, make a few pics 150 | ids = [self.picture.id] 151 | for _ in range(3): 152 | pic = yield Picture(user_id=self.user.id).save() 153 | ids.append(pic.id) 154 | 155 | pics = yield self.user.pictures.get(where=['name = ?', 'a pic']) 156 | self.assertEqual(len(pics), 1) 157 | self.assertEqual(pics[0].name, 'a pic') 158 | 159 | 160 | @inlineCallbacks 161 | def test_has_many_count_with_args(self): 162 | # First, make a few pics 163 | ids = [self.picture.id] 164 | for _ in range(3): 165 | pic = yield Picture(user_id=self.user.id).save() 166 | ids.append(pic.id) 167 | 168 | picsnum = yield self.user.pictures.count(where=['name = ?', 'a pic']) 169 | self.assertEqual(picsnum, 1) 170 | 171 | 172 | @inlineCallbacks 173 | def test_set_has_many(self): 174 | # First, make a few pics 175 | pics = [self.picture] 176 | for _ in range(3): 177 | pic = yield Picture(name="a pic").save() 178 | pics.append(pic) 179 | picids = [int(p.id) for p in pics] 180 | 181 | yield self.user.pictures.set(pics) 182 | results = yield self.user.pictures.get() 183 | resultids = [int(p.id) for p in results] 184 | picids.sort() 185 | resultids.sort() 186 | self.assertEqual(picids, resultids) 187 | 188 | # now try resetting 189 | pics = [] 190 | for _ in range(3): 191 | pic = yield Picture(name="a pic").save() 192 | pics.append(pic) 193 | picids = [p.id for p in pics] 194 | 195 | yield self.user.pictures.set(pics) 196 | results = yield self.user.pictures.get() 197 | resultids = [p.id for p in results] 198 | self.assertEqual(picids, resultids) 199 | 200 | 201 | @inlineCallbacks 202 | def test_clear_has_many(self): 203 | pics = [self.picture] 204 | for _ in range(3): 205 | pic = yield Picture(name="a pic").save() 206 | pics.append(pic) 207 | 208 | yield self.user.pictures.set(pics) 209 | yield self.user.pictures.clear() 210 | 211 | userpics = yield self.user.pictures.get() 212 | self.assertEqual(userpics, []) 213 | 214 | # even go so far as to refetch user 215 | yield User.find(self.user.id) 216 | userpics = yield self.user.pictures.get() 217 | self.assertEqual(userpics, []) 218 | 219 | # picture records should be updated 220 | pics = yield Picture.find(where=["user_id=?", self.user.id]) 221 | self.assertEqual(pics, []) 222 | 223 | # but still exist 224 | pics = yield Picture.all() 225 | self.assertEqual(len(pics), 4) 226 | 227 | 228 | @inlineCallbacks 229 | def test_has_one(self): 230 | avatar = yield self.user.avatar.get() 231 | self.assertEqual(avatar, self.avatar) 232 | 233 | 234 | @inlineCallbacks 235 | def test_set_has_one(self): 236 | avatar = yield Avatar(name="another").save() 237 | yield self.user.avatar.set(avatar) 238 | yield avatar.refresh() 239 | self.assertEqual(avatar.user_id, self.user.id) 240 | 241 | 242 | @inlineCallbacks 243 | def test_habtm(self): 244 | color = yield FavoriteColor(name="red").save() 245 | colors = [self.favcolor, color] 246 | colorids = [c.id for c in colors] 247 | yield FavoriteColor(name="green").save() 248 | 249 | args = {'user_id': self.user.id, 'favorite_color_id': colors[0].id} 250 | yield self.config.insert('favorite_colors_users', args) 251 | args = {'user_id': self.user.id, 'favorite_color_id': colors[1].id} 252 | yield self.config.insert('favorite_colors_users', args) 253 | 254 | newcolors = yield self.user.favorite_colors.get() 255 | newcolorids = [c.id for c in newcolors] 256 | self.assertEqual(newcolorids, colorids) 257 | 258 | 259 | @inlineCallbacks 260 | def test_habtm_with_joinwhere(self): 261 | color = yield FavoriteColor(name="red").save() 262 | colors = [self.favcolor, color] 263 | yield FavoriteColor(name="green").save() 264 | 265 | args = {'user_id': self.user.id, 'favorite_color_id': colors[0].id, 'palette_id': 1} 266 | yield self.config.insert('favorite_colors_users', args) 267 | args = {'user_id': self.user.id, 'favorite_color_id': colors[1].id, 'palette_id': 2} 268 | yield self.config.insert('favorite_colors_users', args) 269 | 270 | newcolors = yield self.user.favorite_colors.get(join_where=['palette_id = ?', 2]) 271 | newcolorids = [c.id for c in newcolors] 272 | self.assertEqual(newcolorids, [colors[1].id]) 273 | 274 | 275 | @inlineCallbacks 276 | def test_habtm_count(self): 277 | color = yield FavoriteColor(name="red").save() 278 | colors = [self.favcolor, color] 279 | yield FavoriteColor(name="green").save() 280 | 281 | args = {'user_id': self.user.id, 'favorite_color_id': colors[0].id} 282 | yield self.config.insert('favorite_colors_users', args) 283 | args = {'user_id': self.user.id, 'favorite_color_id': colors[1].id} 284 | yield self.config.insert('favorite_colors_users', args) 285 | 286 | newcolorsnum = yield self.user.favorite_colors.count() 287 | self.assertEqual(newcolorsnum, 2) 288 | 289 | 290 | @inlineCallbacks 291 | def test_habtm_get_with_args(self): 292 | color = yield FavoriteColor(name="red").save() 293 | colors = [self.favcolor, color] 294 | 295 | args = {'user_id': self.user.id, 'favorite_color_id': colors[0].id} 296 | yield self.config.insert('favorite_colors_users', args) 297 | args = {'user_id': self.user.id, 'favorite_color_id': colors[1].id} 298 | yield self.config.insert('favorite_colors_users', args) 299 | 300 | newcolor = yield self.user.favorite_colors.get(where=['name = ?', 'red'], limit=1) 301 | self.assertEqual(newcolor.id, color.id) 302 | 303 | 304 | @inlineCallbacks 305 | def test_habtm_count_with_args(self): 306 | color = yield FavoriteColor(name="red").save() 307 | colors = [self.favcolor, color] 308 | 309 | args = {'user_id': self.user.id, 'favorite_color_id': colors[0].id} 310 | yield self.config.insert('favorite_colors_users', args) 311 | args = {'user_id': self.user.id, 'favorite_color_id': colors[1].id} 312 | yield self.config.insert('favorite_colors_users', args) 313 | 314 | newcolorsnum = yield self.user.favorite_colors.count(where=['name = ?', 'red']) 315 | self.assertEqual(newcolorsnum, 1) 316 | 317 | 318 | @inlineCallbacks 319 | def test_set_habtm(self): 320 | user = yield User().save() 321 | color = yield FavoriteColor(name="red").save() 322 | colors = [self.favcolor, color] 323 | colorids = [c.id for c in colors] 324 | 325 | yield user.favorite_colors.set(colors) 326 | newcolors = yield user.favorite_colors.get() 327 | newcolorids = [c.id for c in newcolors] 328 | self.assertEqual(newcolorids, colorids) 329 | 330 | 331 | @inlineCallbacks 332 | def test_clear_habtm(self): 333 | user = yield User().save() 334 | color = yield FavoriteColor(name="red").save() 335 | colors = [self.favcolor, color] 336 | 337 | yield user.favorite_colors.set(colors) 338 | yield user.favorite_colors.clear() 339 | colors = yield user.favorite_colors.get() 340 | self.assertEqual(colors, []) 341 | 342 | 343 | @inlineCallbacks 344 | def test_clear_jointable_on_delete_habtm(self): 345 | user = yield User().save() 346 | color = yield FavoriteColor(name="red").save() 347 | colors = [self.favcolor, color] 348 | 349 | yield user.favorite_colors.set(colors) 350 | old_id = color.id 351 | yield color.delete() 352 | result = yield self.config.select('favorite_colors_users', where=['favorite_color_id = ?', old_id], limit=1) 353 | self.assertTrue(result is None) 354 | 355 | 356 | @inlineCallbacks 357 | def test_clear_jointable_on_delete_habtm_with_custom_args(self): 358 | join_tablename = 'posts_categories' 359 | post = yield Blogpost(title='headline').save() 360 | category = yield Category(name="personal").save() 361 | 362 | yield post.categories.set([category]) 363 | cat_id = category.id 364 | yield category.delete() 365 | res = yield self.config.select(join_tablename, where=['category_id = ?', cat_id], limit=1) 366 | self.assertIsNone(res) 367 | 368 | 369 | @inlineCallbacks 370 | def test_set_habtm_blank(self): 371 | user = yield User().save() 372 | color = yield FavoriteColor(name="red").save() 373 | colors = [self.favcolor, color] 374 | 375 | yield user.favorite_colors.set(colors) 376 | # now blank out 377 | yield user.favorite_colors.set([]) 378 | newcolors = yield user.favorite_colors.get() 379 | self.assertEqual(len(newcolors), 0) 380 | -------------------------------------------------------------------------------- /twistar/tests/test_transactions.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twisted.trial import unittest 3 | from twisted.internet.defer import inlineCallbacks 4 | 5 | from twistar.utils import transaction 6 | from twistar.exceptions import TransactionError 7 | 8 | from .utils import initDB, tearDownDB, Registry, Transaction 9 | 10 | 11 | class TransactionTest(unittest.TestCase): 12 | @inlineCallbacks 13 | def setUp(self): 14 | yield initDB(self) 15 | self.config = Registry.getConfig() 16 | 17 | 18 | @inlineCallbacks 19 | def tearDown(self): 20 | yield tearDownDB(self) 21 | 22 | 23 | @inlineCallbacks 24 | def test_findOrCreate(self): 25 | @transaction 26 | @inlineCallbacks 27 | def interaction(txn): 28 | yield Transaction.findOrCreate(name="a name") 29 | yield Transaction.findOrCreate(name="a name") 30 | 31 | yield interaction() 32 | count = yield Transaction.count() 33 | self.assertEqual(count, 1) 34 | 35 | 36 | @inlineCallbacks 37 | def test_doubleInsert(self): 38 | 39 | @transaction 40 | def interaction(txn): 41 | def finish(trans): 42 | return Transaction(name="unique name").save() 43 | return Transaction(name="unique name").save().addCallback(finish) 44 | 45 | try: 46 | yield interaction() 47 | except TransactionError: 48 | pass 49 | 50 | # there should be no transaction records stored at all 51 | count = yield Transaction.count() 52 | self.assertEqual(count, 0) 53 | 54 | 55 | @inlineCallbacks 56 | def test_success(self): 57 | 58 | @transaction 59 | def interaction(txn): 60 | def finish(trans): 61 | return Transaction(name="unique name two").save() 62 | return Transaction(name="unique name").save().addCallback(finish) 63 | 64 | result = yield interaction() 65 | self.assertEqual(result.id, 2) 66 | 67 | count = yield Transaction.count() 68 | self.assertEqual(count, 2) 69 | -------------------------------------------------------------------------------- /twistar/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from twisted.trial import unittest 3 | from twisted.internet.defer import inlineCallbacks 4 | 5 | from twistar import utils 6 | 7 | from .utils import User, initDB, tearDownDB 8 | 9 | from collections import OrderedDict 10 | 11 | 12 | class UtilsTest(unittest.TestCase): 13 | @inlineCallbacks 14 | def setUp(self): 15 | yield initDB(self) 16 | self.user = yield User(first_name="First", last_name="Last", age=10).save() 17 | 18 | 19 | @inlineCallbacks 20 | def test_joinWheres_precedence(self): 21 | yield User(first_name="Second").save() 22 | 23 | first = ['first_name = ?', "First"] 24 | last = ['last_name = ?', "Last"] 25 | second = ['first_name = ?', "Second"] 26 | 27 | last_or_second = utils.joinWheres(last, second, joiner='OR') 28 | where = utils.joinWheres(first, last_or_second, joiner='AND') 29 | 30 | results = yield User.count(where=where) 31 | self.assertEqual(1, results) 32 | 33 | 34 | def test_joinMultipleWheres_empty_arg(self): 35 | where = utils.joinMultipleWheres([], joiner='AND') 36 | self.assertEqual(where, []) 37 | 38 | 39 | def test_joinMultipleWheres_single_where(self): 40 | where = ['first_name = ?', "First"] 41 | joined_where = utils.joinMultipleWheres([where], joiner='AND') 42 | self.assertEqual(where, joined_where) 43 | 44 | 45 | @inlineCallbacks 46 | def test_joinMultipleWheres(self): 47 | yield User(first_name="First", last_name="Last", age=20).save() 48 | 49 | first = ['first_name = ?', "First"] 50 | last = ['last_name = ?', "Last"] 51 | age = ['age <> ?', 20] 52 | 53 | where = utils.joinMultipleWheres([first, last, age], joiner='AND') 54 | 55 | results = yield User.count(where=where) 56 | self.assertEqual(1, results) 57 | 58 | 59 | def test_dictToWhere(self): 60 | self.assertEqual(utils.dictToWhere({}), None) 61 | 62 | result = utils.dictToWhere({'one': 'two'}, "BLAH") 63 | self.assertEqual(result, ["(one = ?)", "two"]) 64 | 65 | result = utils.dictToWhere({'one': None}, "BLAH") 66 | self.assertEqual(result, ["(one is ?)", None]) 67 | 68 | result = utils.dictToWhere(OrderedDict([ 69 | ('one', 'two'), ('three', 'four')])) 70 | self.assertEqual(result, ["(one = ?) AND (three = ?)", "two", "four"]) 71 | 72 | result = utils.dictToWhere(OrderedDict([ 73 | ('one', 'two'), ('three', 'four'), ('five', 'six')]), "BLAH") 74 | self.assertEqual(result, ["(one = ?) BLAH (three = ?) BLAH (five = ?)", "two", "four", "six"]) 75 | 76 | result = utils.dictToWhere(OrderedDict([ 77 | ('one', 'two'), ('three', None)])) 78 | self.assertEqual(result, ["(one = ?) AND (three is ?)", "two", None]) 79 | 80 | 81 | @inlineCallbacks 82 | def tearDown(self): 83 | yield tearDownDB(self) 84 | -------------------------------------------------------------------------------- /twistar/tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function 3 | from twistar.dbobject import DBObject 4 | from twistar.registry import Registry 5 | 6 | import os 7 | 8 | DBTYPE = os.environ.get('DBTYPE', 'sqlite') 9 | if DBTYPE == 'mysql': 10 | print("Using MySQL for tests") 11 | from . import mysql_config 12 | initDB = mysql_config.initDB 13 | tearDownDB = mysql_config.tearDownDB 14 | elif DBTYPE == 'postgres': 15 | print("Using PostgreSQL for tests") 16 | from . import postgres_config 17 | initDB = postgres_config.initDB 18 | tearDownDB = postgres_config.tearDownDB 19 | else: 20 | print("Using SQLite for tests") 21 | from . import sqlite_config 22 | initDB = sqlite_config.initDB 23 | tearDownDB = sqlite_config.initDB 24 | 25 | 26 | class User(DBObject): 27 | HASMANY = ['pictures', 'comments'] 28 | HASONE = ['avatar'] 29 | HABTM = ['favorite_colors'] 30 | 31 | 32 | class Picture(DBObject): 33 | BELONGSTO = ['user'] 34 | 35 | 36 | class Comment(DBObject): 37 | BELONGSTO = ['user'] 38 | 39 | 40 | class Avatar(DBObject): 41 | pass 42 | 43 | 44 | class FavoriteColor(DBObject): 45 | HABTM = ['users'] 46 | 47 | 48 | class Blogpost(DBObject): 49 | HABTM = [dict(name='categories', join_table='posts_categories')] 50 | 51 | 52 | class Category(DBObject): 53 | HABTM = [dict(name='blogposts', join_table='posts_categories')] 54 | 55 | 56 | class FakeObject(DBObject): 57 | pass 58 | 59 | 60 | class Coltest(DBObject): 61 | pass 62 | 63 | 64 | class Transaction(DBObject): 65 | pass 66 | 67 | 68 | class Boy(DBObject): 69 | HASMANY = [{'name': 'nicknames', 'as': 'nicknameable'}] 70 | 71 | 72 | class Girl(DBObject): 73 | HASMANY = [{'name': 'nicknames', 'as': 'nicknameable'}] 74 | 75 | 76 | class Nickname(DBObject): 77 | BELONGSTO = [{'name': 'nicknameable', 'polymorphic': True}] 78 | 79 | 80 | Registry.register(Picture, User, Comment, Avatar, FakeObject, FavoriteColor) 81 | Registry.register(Boy, Girl, Nickname) 82 | Registry.register(Blogpost, Category) 83 | -------------------------------------------------------------------------------- /twistar/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | General catchall for functions that don't make sense as methods. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from twisted.internet import defer, threads, reactor 7 | 8 | from twistar.registry import Registry 9 | from twistar.exceptions import TransactionError 10 | import six 11 | from six.moves import range 12 | from functools import reduce 13 | 14 | 15 | def transaction(interaction): 16 | """ 17 | A decorator to wrap any code in a transaction. If any exceptions are raised, all modifications 18 | are rolled back. The function that is decorated should accept at least one argument, which is 19 | the transaction (in case you want to operate directly on it). 20 | """ 21 | def _transaction(txn, args, kwargs): 22 | config = Registry.getConfig() 23 | config.txn = txn 24 | # get the result of the functions *synchronously*, since this is in a transaction 25 | try: 26 | result = threads.blockingCallFromThread(reactor, interaction, txn, *args, **kwargs) 27 | config.txn = None 28 | return result 29 | except Exception as e: 30 | config.txn = None 31 | raise TransactionError(str(e)) 32 | 33 | def wrapper(*args, **kwargs): 34 | return Registry.DBPOOL.runInteraction(_transaction, args, kwargs) 35 | 36 | return wrapper 37 | 38 | 39 | def createInstances(props, klass): 40 | """ 41 | Create an instance of C{list} of instances of a given class 42 | using the given properties. 43 | 44 | @param props: One of: 45 | 1. A dict, in which case return an instance of klass 46 | 2. A list of dicts, in which case return a list of klass instances 47 | 48 | @return: A C{Deferred} that will pass the result to a callback 49 | """ 50 | if isinstance(props, list): 51 | ks = [klass(**prop) for prop in props] 52 | ds = [defer.maybeDeferred(k.afterInit) for k in ks] 53 | return defer.DeferredList(ds).addCallback(lambda _: ks) 54 | 55 | if props is not None: 56 | k = klass(**props) 57 | return defer.maybeDeferred(k.afterInit).addCallback(lambda _: k) 58 | 59 | return defer.succeed(None) 60 | 61 | 62 | def dictToWhere(attrs, joiner="AND"): 63 | """ 64 | Convert a dictionary of attribute: value to a where statement. 65 | 66 | For instance, dictToWhere({'one': 'two', 'three': 'four'}) returns: 67 | ['(one = ?) AND (three = ?)', 'two', 'four'] 68 | 69 | @return: Expression above if len(attrs) > 0, None otherwise 70 | """ 71 | if len(attrs) == 0: 72 | return None 73 | 74 | wheres = [] 75 | for key, value in six.iteritems(attrs): 76 | comparator = 'is' if value is None else '=' 77 | wheres.append("(%s %s ?)" % (key, comparator)) 78 | 79 | return [(" %s " % joiner).join(wheres)] + list(attrs.values()) 80 | 81 | 82 | def joinWheres(wone, wtwo, joiner="AND"): 83 | """ 84 | Take two wheres (of the same format as the C{where} parameter in the function 85 | L{DBObject.find}) and join them. 86 | 87 | @param wone: First where C{list} 88 | 89 | @param wone: Second where C{list} 90 | 91 | @param joiner: Optional text for joining the two wheres. 92 | 93 | @return: A joined version of the two given wheres. 94 | """ 95 | statement = ["(%s) %s (%s)" % (wone[0], joiner, wtwo[0])] 96 | args = wone[1:] + wtwo[1:] 97 | return statement + args 98 | 99 | 100 | def joinMultipleWheres(wheres, joiner="AND"): 101 | """ 102 | Take a list of wheres (of the same format as the C{where} parameter in the 103 | function L{DBObject.find}) and join them. 104 | 105 | @param wheres: List of where clauses to join C{list} 106 | 107 | @param joiner: Optional text for joining the two wheres. 108 | 109 | @return: A joined version of the list of the given wheres. 110 | """ 111 | wheres = [w for w in wheres if w] # discard empty wheres 112 | if not wheres: 113 | return [] 114 | 115 | return reduce(lambda x, y: joinWheres(x, y, joiner), wheres) 116 | 117 | 118 | def deferredDict(d): 119 | """ 120 | Just like a C{defer.DeferredList} but instead accepts and returns a C{dict}. 121 | 122 | @param d: A C{dict} whose values are all C{Deferred} objects. 123 | 124 | @return: A C{DeferredList} whose callback will be given a dictionary whose 125 | keys are the same as the parameter C{d}'s and whose values are the results 126 | of each individual deferred call. 127 | """ 128 | if len(d) == 0: 129 | return defer.succeed({}) 130 | 131 | def handle(results, names): 132 | rvalue = {} 133 | for index in range(len(results)): 134 | rvalue[names[index]] = results[index][1] 135 | return rvalue 136 | 137 | dl = defer.DeferredList(list(d.values())) 138 | return dl.addCallback(handle, list(d.keys())) 139 | -------------------------------------------------------------------------------- /twistar/validation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Package providing validation support for L{DBObject}s. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from twisted.internet import defer 7 | from BermiInflector.Inflector import Inflector 8 | from twistar.utils import joinWheres, deferredDict 9 | import six 10 | 11 | 12 | def presenceOf(obj, names, kwargs): 13 | """ 14 | A validator to test whether or not some named properties are set. 15 | For those named properties that are not set, an error will 16 | be recorded in C{obj.errors}. 17 | 18 | @param obj: The object whose properties need to be tested. 19 | @param names: The names of the properties to test. 20 | @param kwargs: Keyword arguments. Right now, all but a 21 | C{message} value are ignored. 22 | """ 23 | message = kwargs.get('message', "cannot be blank.") 24 | for name in names: 25 | if getattr(obj, name, "") in ("", None): 26 | obj.errors.add(name, message) 27 | 28 | 29 | def lengthOf(obj, names, kwargs): 30 | """ 31 | A validator to test whether or not some named properties have a 32 | specific length. The length is specified in one of two ways: either 33 | a C{range} keyword set with a C{range} / C{xrange} / C{list} object 34 | containing valid values, or a C{length} keyword with the exact length 35 | allowed. 36 | 37 | For those named properties that do not have the specified length 38 | (or that are C{None}), an error will be recorded in C{obj.errors}. 39 | 40 | @param obj: The object whose properties need to be tested. 41 | @param names: The names of the properties to test. 42 | @param kwargs: Keyword arguments. Right now, all but 43 | C{message}, C{range}, and C{length} values are ignored. 44 | """ 45 | # create a range object representing acceptable values. If 46 | # no range is given (which could be an xrange, range, or list) 47 | # then length is used. If length is not given, a length of 1 is 48 | # assumed 49 | xr = kwargs.get('range', [kwargs.get('length', 1)]) 50 | minmax = (str(min(xr)), str(max(xr))) 51 | if minmax[0] == minmax[1]: 52 | message = kwargs.get('message', "must have a length of %s." % minmax[0]) 53 | else: 54 | message = kwargs.get('message', "must have a length between %s and %s (inclusive)." % minmax) 55 | for name in names: 56 | val = getattr(obj, name, "") 57 | if val is None or not len(val) in xr: 58 | obj.errors.add(name, message) 59 | 60 | 61 | def uniquenessOf(obj, names, kwargs): 62 | """ 63 | A validator to test whether or not some named properties are unique. 64 | For those named properties that are not unique, an error will 65 | be recorded in C{obj.errors}. 66 | 67 | @param obj: The object whose properties need to be tested. 68 | @param names: The names of the properties to test. 69 | @param kwargs: Keyword arguments. Right now, all but a 70 | C{message} value are ignored. 71 | """ 72 | message = kwargs.get('message', "is not unique.") 73 | 74 | def handle(results): 75 | for propname, value in results.items(): 76 | if value is not None: 77 | obj.errors.add(propname, message) 78 | ds = {} 79 | for name in names: 80 | where = ["%s = ?" % name, getattr(obj, name, "")] 81 | if obj.id is not None: 82 | where = joinWheres(where, ["id != ?", obj.id]) 83 | d = obj.__class__.find(where=where, limit=1) 84 | ds[name] = d 85 | return deferredDict(ds).addCallback(handle) 86 | 87 | 88 | 89 | class Validator(object): 90 | """ 91 | A mixin class to handle validating objects before they are saved. 92 | 93 | @cvar VALIDATIONS: A C{list} of functions to call when testing whether or 94 | not a particular instance is valid. 95 | """ 96 | # list of validation methods to call for this class 97 | VALIDATIONS = [] 98 | 99 | @classmethod 100 | def clearValidations(klass): 101 | """ 102 | Clear the given class's validations. 103 | """ 104 | klass.VALIDATIONS = [] 105 | 106 | 107 | @classmethod 108 | def addValidator(klass, func): 109 | """ 110 | Add a function to the given classes validation list. 111 | 112 | @param klass: The Class to add the validator to. 113 | @param func: A function that accepts a single parameter that is the object 114 | to test for validity. If the object is invalid, then this function should 115 | add errors to it's C{errors} property. 116 | 117 | @see: L{Errors} 118 | """ 119 | # Why do this instead of append? you ask. Because, I want a new 120 | # array to be created and assigned (otherwise, all classes will have 121 | # this validator added). 122 | klass.VALIDATIONS = klass.VALIDATIONS + [func] 123 | 124 | 125 | @classmethod 126 | def validatesPresenceOf(klass, *names, **kwargs): 127 | """ 128 | A validator to test whether or not some named properties are set. 129 | For those named properties that are not set, an error will 130 | be recorded in C{obj.errors}. 131 | 132 | @param klass: The klass whose properties need to be tested. 133 | @param names: The names of the properties to test. 134 | @param kwargs: Keyword arguments. Right now, all but a 135 | C{message} value are ignored. 136 | """ 137 | def vfunc(obj): 138 | return presenceOf(obj, names, kwargs) 139 | klass.addValidator(vfunc) 140 | 141 | 142 | @classmethod 143 | def validatesUniquenessOf(klass, *names, **kwargs): 144 | """ 145 | A validator to test whether or not some named properties are unique. 146 | For those named properties that are not unique, an error will 147 | be recorded in C{obj.errors}. 148 | 149 | @param klass: The klass whose properties need to be tested. 150 | @param names: The names of the properties to test. 151 | @param kwargs: Keyword arguments. Right now, all but a 152 | C{message} value are ignored. 153 | """ 154 | def vfunc(obj): 155 | return uniquenessOf(obj, names, kwargs) 156 | klass.addValidator(vfunc) 157 | 158 | 159 | @classmethod 160 | def validatesLengthOf(klass, *names, **kwargs): 161 | """ 162 | A validator to test whether or not some named properties have a 163 | specific length. The length is specified in one of two ways: either 164 | a C{range} keyword set with a C{range} / C{xrange} / C{list} object 165 | containing valid values, or a C{length} keyword with the exact length 166 | allowed. 167 | 168 | For those named properties that do not have 169 | the specified length, an error will be recorded in the instance of C{klass}'s 170 | C{errors} parameter. 171 | 172 | @param klass: The klass whose properties need to be tested. 173 | @param names: The names of the properties to test. 174 | @param kwargs: Keyword arguments. Right now, all but 175 | C{message}, C{range}, and C{length} values are ignored. 176 | """ 177 | def vfunc(obj): 178 | return lengthOf(obj, names, kwargs) 179 | klass.addValidator(vfunc) 180 | 181 | 182 | @classmethod 183 | def _validate(klass, obj): 184 | """ 185 | Validate a given object using all of the set validators for the objects class. 186 | If errors are found, they will be recorded in the objects C{errors} property. 187 | 188 | @return: A C{Deferred} whose callback will receive the given object. 189 | 190 | @see: L{Errors} 191 | """ 192 | ds = [defer.maybeDeferred(func, obj) for func in klass.VALIDATIONS] 193 | # Return the object when finished 194 | return defer.DeferredList(ds).addCallback(lambda results: obj) 195 | 196 | 197 | 198 | class Errors(dict): 199 | """ 200 | A class to hold errors found during validation of a L{DBObject}. 201 | """ 202 | 203 | def __init__(self): 204 | """ 205 | Constructor. 206 | """ 207 | self.infl = Inflector() 208 | 209 | 210 | def add(self, prop, error): 211 | """ 212 | Add an error to a property. The error message stored for this property will be formed 213 | from the humanized name of the property followed by the error message given. For instance, 214 | C{errors.add('first_name', 'cannot be empty')} will result in an error message of 215 | "First Name cannot be empty" being stored for this property. 216 | 217 | @param prop: The name of a property to add an error to. 218 | @param error: A string error to associate with the given property. 219 | """ 220 | self[prop] = self.get(prop, []) 221 | msg = "%s %s" % (self.infl.humanize(prop), str(error)) 222 | if msg not in self[prop]: 223 | self[prop].append(msg) 224 | 225 | 226 | def isEmpty(self): 227 | """ 228 | Returns C{True} if there are any errors associated with any properties, 229 | C{False} otherwise. 230 | """ 231 | for value in six.itervalues(self): 232 | if len(value) > 0: 233 | return False 234 | return True 235 | 236 | 237 | def errorsFor(self, prop): 238 | """ 239 | Get the errors for a specific property. 240 | 241 | @param prop: The property to fetch errors for. 242 | 243 | @return: A C{list} of errors for the given property. If there are none, 244 | then the returned C{list} will have a length of 0. 245 | """ 246 | return self.get(prop, []) 247 | 248 | 249 | def __str__(self): 250 | """ 251 | Return all errors as a single string. 252 | """ 253 | s = [] 254 | for values in six.itervalues(self): 255 | for value in values: 256 | s.append(value) 257 | if len(s) == 0: 258 | return "No errors." 259 | return " ".join(s) 260 | 261 | 262 | def __len__(self): 263 | """ 264 | Get the sum of all errors for all properties. 265 | """ 266 | return sum([len(value) for value in six.itervalues(self)]) 267 | --------------------------------------------------------------------------------