Mercurial > p > roundup > code
comparison roundup/configuration.py @ 6966:8733aa2a8e40
flake8 fixes.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Tue, 13 Sep 2022 16:35:32 -0400 |
| parents | 9ff091537f43 |
| children | 570abc4c6548 |
comparison
equal
deleted
inserted
replaced
| 6965:98d72d4bb489 | 6966:8733aa2a8e40 |
|---|---|
| 31 else: | 31 else: |
| 32 import ConfigParser as configparser # Python 2 | 32 import ConfigParser as configparser # Python 2 |
| 33 | 33 |
| 34 from roundup.exceptions import RoundupException | 34 from roundup.exceptions import RoundupException |
| 35 | 35 |
| 36 ### Exceptions | 36 # Exceptions |
| 37 | 37 |
| 38 | 38 |
| 39 class ConfigurationError(RoundupException): | 39 class ConfigurationError(RoundupException): |
| 40 pass | 40 pass |
| 41 | 41 |
| 117 | 117 |
| 118 | 118 |
| 119 def create_token(size=32): | 119 def create_token(size=32): |
| 120 return b2s(binascii.b2a_base64(random_.token_bytes(size)).strip()) | 120 return b2s(binascii.b2a_base64(random_.token_bytes(size)).strip()) |
| 121 | 121 |
| 122 ### Option classes | 122 # Option classes |
| 123 | 123 |
| 124 | 124 |
| 125 class Option: | 125 class Option: |
| 126 | 126 |
| 127 """Single configuration option. | 127 """Single configuration option. |
| 432 | 432 |
| 433 # FIXME this is the result of running: | 433 # FIXME this is the result of running: |
| 434 # SELECT cfgname FROM pg_ts_config; | 434 # SELECT cfgname FROM pg_ts_config; |
| 435 # on a postgresql 14.1 server. | 435 # on a postgresql 14.1 server. |
| 436 # So the best we can do is hardcode this. | 436 # So the best we can do is hardcode this. |
| 437 valid_langs = [ "simple", | 437 valid_langs = ["simple", |
| 438 "custom1", | 438 "custom1", |
| 439 "custom2", | 439 "custom2", |
| 440 "custom3", | 440 "custom3", |
| 441 "custom4", | 441 "custom4", |
| 442 "custom5", | 442 "custom5", |
| 443 "arabic", | 443 "arabic", |
| 444 "armenian", | 444 "armenian", |
| 445 "basque", | 445 "basque", |
| 446 "catalan", | 446 "catalan", |
| 447 "danish", | 447 "danish", |
| 448 "dutch", | 448 "dutch", |
| 449 "english", | 449 "english", |
| 450 "finnish", | 450 "finnish", |
| 451 "french", | 451 "french", |
| 452 "german", | 452 "german", |
| 453 "greek", | 453 "greek", |
| 454 "hindi", | 454 "hindi", |
| 455 "hungarian", | 455 "hungarian", |
| 456 "indonesian", | 456 "indonesian", |
| 457 "irish", | 457 "irish", |
| 458 "italian", | 458 "italian", |
| 459 "lithuanian", | 459 "lithuanian", |
| 460 "nepali", | 460 "nepali", |
| 461 "norwegian", | 461 "norwegian", |
| 462 "portuguese", | 462 "portuguese", |
| 463 "romanian", | 463 "romanian", |
| 464 "russian", | 464 "russian", |
| 465 "serbian", | 465 "serbian", |
| 466 "spanish", | 466 "spanish", |
| 467 "swedish", | 467 "swedish", |
| 468 "tamil", | 468 "tamil", |
| 469 "turkish", | 469 "turkish", |
| 470 "yiddish" ] | 470 "yiddish"] |
| 471 | 471 |
| 472 def str2value(self, value): | 472 def str2value(self, value): |
| 473 _val = value.lower() | 473 _val = value.lower() |
| 474 if _val in self.allowed: | 474 if _val in self.allowed: |
| 475 return _val | 475 return _val |
| 476 raise OptionValueError(self, value, self.class_description) | 476 raise OptionValueError(self, value, self.class_description) |
| 493 lang_avail = b2s(xapian.Stem.get_available_languages()) | 493 lang_avail = b2s(xapian.Stem.get_available_languages()) |
| 494 languages = textwrap.fill(_("Valid languages: ") + | 494 languages = textwrap.fill(_("Valid languages: ") + |
| 495 lang_avail, 75, | 495 lang_avail, 75, |
| 496 subsequent_indent=" ") | 496 subsequent_indent=" ") |
| 497 raise OptionValueError(options["INDEXER_LANGUAGE"], | 497 raise OptionValueError(options["INDEXER_LANGUAGE"], |
| 498 lang, languages) | 498 lang, languages) |
| 499 | 499 |
| 500 if self._value == "native-fts": | 500 if self._value == "native-fts": |
| 501 lang = options["INDEXER_LANGUAGE"]._value | 501 lang = options["INDEXER_LANGUAGE"]._value |
| 502 if lang not in self.valid_langs: | 502 if lang not in self.valid_langs: |
| 503 import textwrap | 503 import textwrap |
| 504 languages = textwrap.fill(_("Expected languages: ") + | 504 languages = textwrap.fill(_("Expected languages: ") + |
| 505 " ".join(self.valid_langs), 75, | 505 " ".join(self.valid_langs), 75, |
| 506 subsequent_indent=" ") | 506 subsequent_indent=" ") |
| 507 raise OptionValueError(options["INDEXER_LANGUAGE"], | 507 raise OptionValueError(options["INDEXER_LANGUAGE"], |
| 508 lang, languages) | 508 lang, languages) |
| 509 | |
| 509 | 510 |
| 510 class MailAddressOption(Option): | 511 class MailAddressOption(Option): |
| 511 | 512 |
| 512 """Email address | 513 """Email address |
| 513 | 514 |
| 538 _val = Option.get(self) | 539 _val = Option.get(self) |
| 539 if _val and not os.path.isabs(_val): | 540 if _val and not os.path.isabs(_val): |
| 540 _val = os.path.join(self.config["HOME"], _val) | 541 _val = os.path.join(self.config["HOME"], _val) |
| 541 return _val | 542 return _val |
| 542 | 543 |
| 544 | |
| 543 class SpaceSeparatedListOption(Option): | 545 class SpaceSeparatedListOption(Option): |
| 544 | 546 |
| 545 """List of space seperated elements. | 547 """List of space seperated elements. |
| 546 """ | 548 """ |
| 547 | 549 |
| 549 | 551 |
| 550 def get(self): | 552 def get(self): |
| 551 pathlist = [] | 553 pathlist = [] |
| 552 _val = Option.get(self) | 554 _val = Option.get(self) |
| 553 for elem in _val.split(): | 555 for elem in _val.split(): |
| 554 pathlist.append(elem) | 556 pathlist.append(elem) |
| 555 if pathlist: | 557 if pathlist: |
| 556 return pathlist | 558 return pathlist |
| 557 else: | 559 else: |
| 558 return None | 560 return None |
| 559 | 561 |
| 562 | |
| 560 class OriginHeadersListOption(Option): | 563 class OriginHeadersListOption(Option): |
| 561 | 564 |
| 562 """List of space seperated origin header values. | 565 """List of space seperated origin header values. |
| 563 """ | 566 """ |
| 564 | 567 |
| 565 class_description = "A list of space separated case sensitive origin headers 'scheme://host'." | 568 class_description = "A list of space separated case sensitive origin headers 'scheme://host'." |
| 566 | |
| 567 | 569 |
| 568 def set(self, _val): | 570 def set(self, _val): |
| 569 pathlist = self._value = [] | 571 pathlist = self._value = [] |
| 570 for elem in _val.split(): | 572 for elem in _val.split(): |
| 571 pathlist.append(elem) | 573 pathlist.append(elem) |
| 572 if '*' in pathlist and len(pathlist) != 1: | 574 if '*' in pathlist and len(pathlist) != 1: |
| 573 raise OptionValueError(self, _val, | 575 raise OptionValueError(self, _val, |
| 574 "If using '*' it must be the only element.") | 576 "If using '*' it must be the only element.") |
| 575 | 577 |
| 576 def _value2str(self, value): | 578 def _value2str(self, value): |
| 577 return ','.join(value) | 579 return ','.join(value) |
| 580 | |
| 578 | 581 |
| 579 class MultiFilePathOption(Option): | 582 class MultiFilePathOption(Option): |
| 580 | 583 |
| 581 """List of space seperated File or directory path name | 584 """List of space seperated File or directory path name |
| 582 | 585 |
| 682 allow the config file to be stored in version control without | 685 allow the config file to be stored in version control without |
| 683 storing the secret there. | 686 storing the secret there. |
| 684 | 687 |
| 685 """ | 688 """ |
| 686 | 689 |
| 687 class_description = \ | 690 class_description = ( |
| 688 "A string that starts with 'file://' is interpreted as a file path \n" \ | 691 "A string that starts with 'file://' is interpreted\n" |
| 689 "relative to the tracker home. Using 'file:///' defines an absolute \n" \ | 692 "as a file path relative to the tracker home. Using\n" |
| 690 "path. The first line of the file will be used as the value. Any \n" \ | 693 "'file:///' defines an absolute path. The first\n" |
| 691 "string that does not start with 'file://' is used as is. It \n" \ | 694 "line of the file will be used as the value. Any\n" |
| 692 "removes any whitespace at the end of the line, so a newline can \n" \ | 695 "string that does not start with 'file://' is used\n" |
| 693 "be put in the file.\n" | 696 "as is. It removes any whitespace at the end of the\n" |
| 697 "line, so a newline can be put in the file.\n") | |
| 694 | 698 |
| 695 def get(self): | 699 def get(self): |
| 696 _val = Option.get(self) | 700 _val = Option.get(self) |
| 697 if isinstance(_val, str) and _val.startswith('file://'): | 701 if isinstance(_val, str) and _val.startswith('file://'): |
| 698 filepath = _val[7:] | 702 filepath = _val[7:] |
| 705 # compatible version | 709 # compatible version |
| 706 except EnvironmentError as e: | 710 except EnvironmentError as e: |
| 707 if e.errno != errno.ENOENT: | 711 if e.errno != errno.ENOENT: |
| 708 raise | 712 raise |
| 709 else: | 713 else: |
| 710 raise OptionValueError(self, _val, | 714 raise OptionValueError( |
| 715 self, _val, | |
| 711 "Unable to read value for %s. Error opening " | 716 "Unable to read value for %s. Error opening " |
| 712 "%s: %s." % (self.name, e.filename, e.args[1])) | 717 "%s: %s." % (self.name, e.filename, e.args[1])) |
| 713 return self.str2value(_val) | 718 return self.str2value(_val) |
| 714 | 719 |
| 715 def validate(self, options): | 720 def validate(self, options): |
| 720 # is set. | 725 # is set. |
| 721 try: | 726 try: |
| 722 self.get() | 727 self.get() |
| 723 except OptionUnsetError: | 728 except OptionUnsetError: |
| 724 # provide error message with link to MAIL_USERNAME | 729 # provide error message with link to MAIL_USERNAME |
| 725 raise OptionValueError(options["MAIL_PASSWORD"], | 730 raise OptionValueError( |
| 726 "not defined", | 731 options["MAIL_PASSWORD"], |
| 727 "Mail username is set, so this must be defined.") | 732 "not defined", |
| 733 "Mail username is set, so this must be defined.") | |
| 728 else: | 734 else: |
| 729 self.get() | 735 self.get() |
| 730 | 736 |
| 731 | 737 |
| 732 class WebUrlOption(Option): | 738 class WebUrlOption(Option): |
| 800 class SecretNullableOption(NullableOption, SecretOption): | 806 class SecretNullableOption(NullableOption, SecretOption): |
| 801 # use get from SecretOption and rest from NullableOption | 807 # use get from SecretOption and rest from NullableOption |
| 802 get = SecretOption.get | 808 get = SecretOption.get |
| 803 class_description = SecretOption.class_description | 809 class_description = SecretOption.class_description |
| 804 | 810 |
| 811 | |
| 805 class RedisUrlOption(SecretNullableOption): | 812 class RedisUrlOption(SecretNullableOption): |
| 806 """Do required check to make sure known bad parameters are not | 813 """Do required check to make sure known bad parameters are not |
| 807 put in the url. | 814 put in the url. |
| 808 | 815 |
| 809 Should I do more URL validation? Validate schema: | 816 Should I do more URL validation? Validate schema: |
| 810 redis, rediss, unix? How many cycles to invest | 817 redis, rediss, unix? How many cycles to invest |
| 811 to keep users from their own mistakes? | 818 to keep users from their own mistakes? |
| 812 """ | 819 """ |
| 813 | 820 |
| 814 class_description = SecretNullableOption.class_description | 821 class_description = SecretNullableOption.class_description |
| 815 | 822 |
| 816 def str2value(self, value): | 823 def str2value(self, value): |
| 817 if value and value.find("decode_responses") != -1: | 824 if value and value.find("decode_responses") != -1: |
| 818 raise OptionValueError(self, value, "URL must not include " | 825 raise OptionValueError(self, value, "URL must not include " |
| 819 "decode_responses. Please remove " | 826 "decode_responses. Please remove " |
| 820 "the option.") | 827 "the option.") |
| 821 return value | 828 return value |
| 829 | |
| 822 | 830 |
| 823 class SessiondbBackendOption(Option): | 831 class SessiondbBackendOption(Option): |
| 824 """Make sure that sessiondb is compatile with the primary db. | 832 """Make sure that sessiondb is compatile with the primary db. |
| 825 Fail with error and suggestions if they are incompatible. | 833 Fail with error and suggestions if they are incompatible. |
| 826 """ | 834 """ |
| 850 # unset will choose default | 858 # unset will choose default |
| 851 return | 859 return |
| 852 | 860 |
| 853 redis_available = False | 861 redis_available = False |
| 854 try: | 862 try: |
| 855 import redis | 863 import redis # noqa: F401 |
| 856 redis_available = True | 864 redis_available = True |
| 857 except ImportError: | 865 except ImportError: |
| 858 if sessiondb_backend == 'redis': | 866 if sessiondb_backend == 'redis': |
| 859 valid_session_backends = ', '.join(sorted(list( | 867 valid_session_backends = ', '.join(sorted(list( |
| 860 [ x[1] for x in self.compatibility_matrix | 868 [x[1] for x in self.compatibility_matrix |
| 861 if x[0] == rdbms_backend and x[1] != 'redis']) | 869 if x[0] == rdbms_backend and x[1] != 'redis']) |
| 862 )) | 870 )) |
| 863 raise OptionValueError(self, sessiondb_backend, | 871 raise OptionValueError( |
| 864 "Unable to load redis module. Please install " | 872 self, sessiondb_backend, |
| 865 "a redis library or choose\n an alternate " | 873 "Unable to load redis module. Please install " |
| 866 "session db: %(valid_session_backends)s"%locals()) | 874 "a redis library or choose\n an alternate " |
| 867 | 875 "session db: %(valid_session_backends)s" % locals()) |
| 868 if ( (rdbms_backend, sessiondb_backend) not in | 876 |
| 869 self.compatibility_matrix ): | 877 if ((rdbms_backend, sessiondb_backend) not in |
| 878 self.compatibility_matrix): | |
| 870 | 879 |
| 871 valid_session_backends = ', '.join(sorted(list( | 880 valid_session_backends = ', '.join(sorted(list( |
| 872 set([ x[1] for x in self.compatibility_matrix | 881 set([x[1] for x in self.compatibility_matrix |
| 873 if x[0] == rdbms_backend and | 882 if x[0] == rdbms_backend and |
| 874 ( redis_available or x[1] != 'redis')]) | 883 (redis_available or x[1] != 'redis')]) |
| 875 ))) | 884 ))) |
| 876 | 885 |
| 877 raise OptionValueError(self, sessiondb_backend, | 886 raise OptionValueError( |
| 887 self, sessiondb_backend, | |
| 878 "You can not use session db type: %(sessiondb_backend)s " | 888 "You can not use session db type: %(sessiondb_backend)s " |
| 879 "with %(rdbms_backend)s.\n Valid session db types: " | 889 "with %(rdbms_backend)s.\n Valid session db types: " |
| 880 "%(valid_session_backends)s."%locals()) | 890 "%(valid_session_backends)s." % locals()) |
| 881 | 891 |
| 882 | 892 |
| 883 class TimezoneOption(Option): | 893 class TimezoneOption(Option): |
| 884 | 894 |
| 885 class_description = \ | 895 class_description = \ |
| 898 | 908 |
| 899 def str2value(self, value): | 909 def str2value(self, value): |
| 900 try: | 910 try: |
| 901 roundup.date.get_timezone(value) | 911 roundup.date.get_timezone(value) |
| 902 except KeyError: | 912 except KeyError: |
| 903 raise OptionValueError(self, value, | 913 raise OptionValueError( |
| 904 "Timezone name or numeric hour offset required") | 914 self, value, |
| 915 "Timezone name or numeric hour offset required") | |
| 905 return value | 916 return value |
| 906 | 917 |
| 907 | 918 |
| 908 class HttpVersionOption(Option): | 919 class HttpVersionOption(Option): |
| 909 """Used by roundup-server to verify http version is set to valid | 920 """Used by roundup-server to verify http version is set to valid |
| 910 string.""" | 921 string.""" |
| 911 | 922 |
| 912 def str2value(self, value): | 923 def str2value(self, value): |
| 913 if value not in ["HTTP/1.0", "HTTP/1.1"]: | 924 if value not in ["HTTP/1.0", "HTTP/1.1"]: |
| 914 raise OptionValueError(self, value, | 925 raise OptionValueError( |
| 926 self, value, | |
| 915 "Valid vaues for -V or --http_version are: HTTP/1.0, HTTP/1.1") | 927 "Valid vaues for -V or --http_version are: HTTP/1.0, HTTP/1.1") |
| 916 return value | 928 return value |
| 917 | 929 |
| 918 | 930 |
| 919 class RegExpOption(Option): | 931 class RegExpOption(Option): |
| 945 value.decode("ascii") | 957 value.decode("ascii") |
| 946 except UnicodeError: | 958 except UnicodeError: |
| 947 value = value.decode("utf-8") | 959 value = value.decode("utf-8") |
| 948 return re.compile(value, self.flags) | 960 return re.compile(value, self.flags) |
| 949 | 961 |
| 962 | |
| 950 try: | 963 try: |
| 951 import jinja2 | 964 import jinja2 # noqa: F401 |
| 952 jinja2_avail = "Available found" | 965 jinja2_avail = "Available found" |
| 953 except ImportError: | 966 except ImportError: |
| 954 jinja2_avail = "Unavailable needs" | 967 jinja2_avail = "Unavailable needs" |
| 955 | 968 |
| 956 ### Main configuration layout. | 969 # Main configuration layout. |
| 957 # Config is described as a sequence of sections, | 970 # Config is described as a sequence of sections, |
| 958 # where each section name is followed by a sequence | 971 # where each section name is followed by a sequence |
| 959 # of Option definitions. Each Option definition | 972 # of Option definitions. Each Option definition |
| 960 # is a sequence containing class name and constructor | 973 # is a sequence containing class name and constructor |
| 961 # parameters, starting from the setting name: | 974 # parameters, starting from the setting name: |
| 971 "Templating engine to use.\n" | 984 "Templating engine to use.\n" |
| 972 "Possible values are:\n" | 985 "Possible values are:\n" |
| 973 " 'zopetal' for the old TAL engine ported from Zope,\n" | 986 " 'zopetal' for the old TAL engine ported from Zope,\n" |
| 974 " 'chameleon' for Chameleon,\n" | 987 " 'chameleon' for Chameleon,\n" |
| 975 " 'jinja2' for jinja2 templating.\n" | 988 " 'jinja2' for jinja2 templating.\n" |
| 976 " %s jinja2 module."%jinja2_avail), | 989 " %s jinja2 module." % jinja2_avail), |
| 977 (FilePathOption, "templates", "html", | 990 (FilePathOption, "templates", "html", |
| 978 "Path to the HTML templates directory."), | 991 "Path to the HTML templates directory."), |
| 979 (MultiFilePathOption, "static_files", "", | 992 (MultiFilePathOption, "static_files", "", |
| 980 "A list of space separated directory paths (or a single\n" | 993 "A list of space separated directory paths (or a single\n" |
| 981 "directory). These directories hold additional static\n" | 994 "directory). These directories hold additional static\n" |
| 1703 "text with <br>. Set true if you want GitHub Flavored Markdown\n" | 1716 "text with <br>. Set true if you want GitHub Flavored Markdown\n" |
| 1704 "(GFM) handling of embedded newlines."), | 1717 "(GFM) handling of embedded newlines."), |
| 1705 ), "Markdown rendering options."), | 1718 ), "Markdown rendering options."), |
| 1706 ) | 1719 ) |
| 1707 | 1720 |
| 1708 ### Configuration classes | 1721 # Configuration classes |
| 1709 | 1722 |
| 1710 | 1723 |
| 1711 class Config: | 1724 class Config: |
| 1712 | 1725 |
| 1713 """Base class for configuration objects. | 1726 """Base class for configuration objects. |
| 2198 self.init_logging() | 2211 self.init_logging() |
| 2199 | 2212 |
| 2200 def init_logging(self): | 2213 def init_logging(self): |
| 2201 _file = self["LOGGING_CONFIG"] | 2214 _file = self["LOGGING_CONFIG"] |
| 2202 if _file and os.path.isfile(_file): | 2215 if _file and os.path.isfile(_file): |
| 2203 logging.config.fileConfig(_file, | 2216 logging.config.fileConfig( |
| 2204 disable_existing_loggers=self["LOGGING_DISABLE_LOGGERS"]) | 2217 _file, |
| 2218 disable_existing_loggers=self["LOGGING_DISABLE_LOGGERS"]) | |
| 2205 return | 2219 return |
| 2206 | 2220 |
| 2207 _file = self["LOGGING_FILENAME"] | 2221 _file = self["LOGGING_FILENAME"] |
| 2208 # set file & level on the roundup logger | 2222 # set file & level on the roundup logger |
| 2209 logger = logging.getLogger('roundup') | 2223 logger = logging.getLogger('roundup') |
