From decf1d1a4020ee64e669608acae299632432aae8 Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 13:25:38 +0600 Subject: [PATCH 01/37] add new configuration project --- .../pyRevitLabs.Common.csproj | 4 - .../Extensions/JsonConfigurationExtensions.cs | 15 ++ .../JsonConfiguration.cs | 103 +++++++++++++ .../pyRevitLabs.Configurations.Json.csproj | 18 +++ .../Extensions/YamlConfigurationExtensions.cs | 15 ++ .../YamlConfiguration.cs | 143 ++++++++++++++++++ .../pyRevitLabs.Configurations.Yaml.csproj | 18 +++ .../Abstractions/IConfiguration.cs | 13 ++ .../ConfigurationBase.cs | 121 +++++++++++++++ .../ConfigurationBuilder.cs | 22 +++ .../ConfigurationService.cs | 95 ++++++++++++ .../Exceptions/ConfigurationException.cs | 10 ++ ...onfigurationSectionKeyNotFoundException.cs | 13 ++ .../ConfigurationSectionNotFoundException.cs | 13 ++ .../Extensions/ConfigurationExtensions.cs | 6 + .../pyRevitLabs.Configurations.csproj | 10 ++ dev/pyRevitLabs/pyRevitLabs.sln | 56 +++++++ .../IniConfigurationUnitTests.cs | 60 ++++++++ .../IniCreateFixture.cs | 21 +++ ...yRevitLabs.Configurations.Ini.Tests.csproj | 29 ++++ .../JsonConfigurationUnitTests.cs | 60 ++++++++ .../JsonCreateFixture.cs | 21 +++ ...RevitLabs.Configurations.Json.Tests.csproj | 29 ++++ .../ConfigurationServiceFixture.cs | 50 ++++++ .../ConfigurationServiceUnitTests.cs | 12 ++ .../ConfigurationTests.cs | 91 +++++++++++ .../pyRevitLabs.Configurations.Tests.csproj | 27 ++++ .../YamlConfigurationUnitTests.cs | 60 ++++++++ .../YamlCreateFixture.cs | 21 +++ ...RevitLabs.Configurations.Yaml.Tests.csproj | 29 ++++ 30 files changed, 1181 insertions(+), 4 deletions(-) create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations.Json/Extensions/JsonConfigurationExtensions.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations.Json/pyRevitLabs.Configurations.Json.csproj create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/Extensions/YamlConfigurationExtensions.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/pyRevitLabs.Configurations.Yaml.csproj create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationException.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationSectionKeyNotFoundException.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationSectionNotFoundException.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/Extensions/ConfigurationExtensions.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/pyRevitLabs.Configurations.csproj create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniCreateFixture.cs create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/pyRevitLabs.Configurations.Ini.Tests.csproj create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonCreateFixture.cs create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/pyRevitLabs.Configurations.Json.Tests.csproj create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceUnitTests.cs create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationTests.cs create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/pyRevitLabs.Configurations.Tests.csproj create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlCreateFixture.cs create mode 100644 dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj diff --git a/dev/pyRevitLabs/pyRevitLabs.Common/pyRevitLabs.Common.csproj b/dev/pyRevitLabs/pyRevitLabs.Common/pyRevitLabs.Common.csproj index 1002cf1e1..cd8027a64 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Common/pyRevitLabs.Common.csproj +++ b/dev/pyRevitLabs/pyRevitLabs.Common/pyRevitLabs.Common.csproj @@ -17,9 +17,6 @@ - - - @@ -36,7 +33,6 @@ - diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/Extensions/JsonConfigurationExtensions.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/Extensions/JsonConfigurationExtensions.cs new file mode 100644 index 000000000..c95207396 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/Extensions/JsonConfigurationExtensions.cs @@ -0,0 +1,15 @@ +namespace pyRevitLabs.Configurations.Json.Extensions; + +public static class JsonConfigurationExtensions +{ + public static ConfigurationBuilder AddJsonConfiguration(this ConfigurationBuilder builder, string configurationPath, bool readOnly = default) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); + + if (string.IsNullOrWhiteSpace(configurationPath)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(configurationPath)); + + return builder.AddConfigurationSource(JsonConfiguration.Create(configurationPath, readOnly)); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs new file mode 100644 index 000000000..ef06b2ad5 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs @@ -0,0 +1,103 @@ +using System.Text; +using System.Xml; +using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Exceptions; +using pyRevitLabs.Json; +using pyRevitLabs.Json.Linq; +using Formatting = pyRevitLabs.Json.Formatting; + +namespace pyRevitLabs.Configurations.Json; + +public sealed class JsonConfiguration : ConfigurationBase +{ + public static readonly Encoding DefaultJsonFileEncoding = Encoding.UTF8; + + private readonly JObject _jsonObject; + + /// + /// Create json configuration instance. + /// + /// Configuration path. + /// Readonly configurations + private JsonConfiguration(string configurationPath, bool readOnly) + : base(configurationPath, readOnly) + { + _jsonObject = !File.Exists(configurationPath) + ? new JObject() + : JObject.Parse(File.ReadAllText(_configurationPath, DefaultJsonFileEncoding)); + } + + /// + /// Creates JsonConfiguration. + /// + /// Configuration file path. + /// Mark file is readonly. + /// Return new JsonConfiguration. + /// When configurationPath is null. + public static IConfiguration Create(string configurationPath, bool readOnly = default) + { + if (configurationPath is null) + throw new ArgumentNullException(nameof(configurationPath)); + + return new JsonConfiguration(configurationPath, readOnly); + } + + protected override void SaveConfigurationImpl() + { + string jsonString = JsonConvert.SerializeObject(_jsonObject, + new JsonSerializerSettings() {Formatting = Formatting.Indented}); + File.WriteAllText(_configurationPath, jsonString, DefaultJsonFileEncoding); + } + + protected override bool HasSectionImpl(string sectionName) + { + return _jsonObject.ContainsKey(sectionName); + } + + protected override bool HasSectionKeyImpl(string sectionName, string keyName) + { + JObject? sectionObject = _jsonObject[sectionName] as JObject; + return sectionObject?.ContainsKey(keyName) == true; + } + + protected override bool RemoveValueImpl(string sectionName, string keyName) + { + _jsonObject[sectionName]![keyName] = null; + return true; + } + + protected override T GetValueImpl(string sectionName, string keyName) + { + JToken? token = _jsonObject[sectionName]?[keyName]; + return token is null + ? throw new ConfigurationException($"Section {sectionName} or keyName {keyName} not found.") + : token.ToObject() + ?? throw new ConfigurationException($"Cannot deserialize value with {sectionName} and {keyName}."); + } + + protected override void SetValueImpl(string sectionName, string keyName, T value) + { + if (!HasSection(sectionName)) + { + JObject fromObject = new(); + fromObject.Add(keyName, JToken.FromObject(value!)); + + _jsonObject.Add(sectionName, fromObject); + + return; + } + + if (!HasSectionKey(sectionName, keyName)) + { + JObject fromObject = new(); + fromObject.Add(keyName, JToken.FromObject(value!)); + + JObject? sectionObject = (JObject?)_jsonObject[sectionName]; + sectionObject?.Add(keyName, fromObject); + + return; + } + + _jsonObject[sectionName]![keyName] = JToken.FromObject(value!); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/pyRevitLabs.Configurations.Json.csproj b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/pyRevitLabs.Configurations.Json.csproj new file mode 100644 index 000000000..8e925e100 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/pyRevitLabs.Configurations.Json.csproj @@ -0,0 +1,18 @@ + + + + net48;net8.0 + enable + enable + 12 + + + + + + + + + + + diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/Extensions/YamlConfigurationExtensions.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/Extensions/YamlConfigurationExtensions.cs new file mode 100644 index 000000000..ae696c77b --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/Extensions/YamlConfigurationExtensions.cs @@ -0,0 +1,15 @@ +namespace pyRevitLabs.Configurations.Yaml.Extensions; + +public static class YamlConfigurationExtensions +{ + public static ConfigurationBuilder AddYamlConfiguration(this ConfigurationBuilder builder, string configurationPath, bool readOnly = default) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); + + if (string.IsNullOrWhiteSpace(configurationPath)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(configurationPath)); + + return builder.AddConfigurationSource(YamlConfiguration.Create(configurationPath, readOnly)); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs new file mode 100644 index 000000000..5f2312563 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs @@ -0,0 +1,143 @@ +using System.Text; +using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Exceptions; +using System.Collections; +using YamlDotNet.Core; +using YamlDotNet.RepresentationModel; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace pyRevitLabs.Configurations.Yaml; + +public sealed class YamlConfiguration : ConfigurationBase +{ + public static readonly Encoding DefaultYamlFileEncoding = Encoding.UTF8; + + private readonly YamlStream _yamlStream; + private readonly YamlMappingNode _rootNode; + + /// + /// Create yaml configuration instance. + /// + /// Configuration path. + /// Admin configurations + private YamlConfiguration(string configurationPath, bool readOnly) + : base(configurationPath, readOnly) + { + _yamlStream = []; + if (File.Exists(configurationPath)) + { + _yamlStream.Load(new StringReader(File.ReadAllText(_configurationPath, DefaultYamlFileEncoding))); + } + + if (_yamlStream.Documents.Count == 0) + { + _yamlStream.Documents.Add(new YamlDocument(new YamlMappingNode())); + } + + _rootNode = (YamlMappingNode)_yamlStream.Documents[0].RootNode; + } + + /// + /// Creates YamlConfiguration. + /// + /// Configuration file path. + /// Mark file is readonly. + /// Return new YamlConfiguration. + /// When configurationPath is null. + public static IConfiguration Create(string configurationPath, bool readOnly = default) + { + if (configurationPath is null) + throw new ArgumentNullException(nameof(configurationPath)); + + return new YamlConfiguration(configurationPath, readOnly); + } + + protected override void SaveConfigurationImpl() + { + ISerializer serializer = CreateSerializer(); + string yamlString = serializer.Serialize(_yamlStream); + File.WriteAllText(_configurationPath, yamlString, DefaultYamlFileEncoding); + } + + protected override bool HasSectionImpl(string sectionName) + { + return _rootNode.Children.ContainsKey(sectionName) + && _rootNode.Children[sectionName].NodeType == YamlNodeType.Mapping; + } + + protected override bool HasSectionKeyImpl(string sectionName, string keyName) + { + if (!HasSection(sectionName)) + { + return false; + } + + YamlMappingNode sectionNode = (YamlMappingNode)_rootNode.Children[sectionName]; + if (!sectionNode.Children.ContainsKey(keyName)) + { + return false; + } + + YamlNodeType? yamlNodeType = sectionNode.Children[keyName].NodeType; + return yamlNodeType is YamlNodeType.Scalar or YamlNodeType.Sequence or YamlNodeType.Mapping; + } + + protected override bool RemoveValueImpl(string sectionName, string keyName) + { + YamlMappingNode yamlNode = (YamlMappingNode)_rootNode[sectionName]; + return yamlNode.Children.Remove(keyName); + } + + protected override T GetValueImpl(string sectionName, string keyName) + { + YamlNode yamlNode = _rootNode[sectionName][keyName]; + return CreateDeserializer().Deserialize(yamlNode.ToString()); + } + + protected override void SetValueImpl(string sectionName, string keyName, T value) + { + if (_yamlStream.Documents.Count == 0) + _yamlStream.Documents.Add(new YamlDocument(new YamlMappingNode())); + + if (value is string stringValue) + { + if (string.IsNullOrEmpty(stringValue)) + { + return; + } + } + + if (!HasSection(sectionName)) + { + _rootNode.Add(sectionName, + new YamlMappingNode( + new KeyValuePair(keyName, YamlMappingNode.FromObject(value!)))); + } + + if (!HasSectionKey(sectionName, keyName)) + { + YamlMappingNode sectionNode = (YamlMappingNode)_rootNode[sectionName]; + sectionNode.Add(keyName, YamlMappingNode.FromObject(value!)); + } + + RemoveValue(sectionName, keyName); + + YamlMappingNode sectionNode1 = (YamlMappingNode)_rootNode[sectionName]; + sectionNode1.Add(keyName, YamlMappingNode.FromObject(value!)); + } + + private static ISerializer CreateSerializer() + { + return new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + } + + private static IDeserializer CreateDeserializer() + { + return new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/pyRevitLabs.Configurations.Yaml.csproj b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/pyRevitLabs.Configurations.Yaml.csproj new file mode 100644 index 000000000..0cdc75615 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/pyRevitLabs.Configurations.Yaml.csproj @@ -0,0 +1,18 @@ + + + + net48;net8.0 + enable + enable + 12 + + + + + + + + + + + diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs new file mode 100644 index 000000000..b78c2275c --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs @@ -0,0 +1,13 @@ +namespace pyRevitLabs.Configurations.Abstractions; + +public interface IConfiguration +{ + bool HasSection(string sectionName); + bool HasSectionKey(string sectionName, string keyName); + + T GetValue(string sectionName, string keyName); + T? GetValueOrDefault(string sectionName, string keyName, T? defaultValue = default); + + bool RemoveValue(string sectionName, string keyName); + void SetValue(string sectionName, string keyName, T? value); +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs new file mode 100644 index 000000000..10a7c0824 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs @@ -0,0 +1,121 @@ +using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Exceptions; + +namespace pyRevitLabs.Configurations; + +public abstract class ConfigurationBase(string configurationPath, bool readOnly) : IConfiguration +{ + protected readonly string _configurationPath = configurationPath; + + public bool ReadOnly { get; } = readOnly; + + public void SaveConfiguration() + { + if (ReadOnly) + { + return; + } + + SaveConfigurationImpl(); + } + + /// + public bool HasSection(string sectionName) + { + if (string.IsNullOrWhiteSpace(sectionName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(sectionName)); + + return HasSectionImpl(sectionName); + } + + /// + public bool HasSectionKey(string sectionName, string keyName) + { + if (string.IsNullOrWhiteSpace(keyName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(keyName)); + + return HasSectionKeyImpl(sectionName, keyName); + } + + /// + public bool RemoveValue(string sectionName, string keyName) + { + if (string.IsNullOrWhiteSpace(sectionName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(sectionName)); + + if (string.IsNullOrWhiteSpace(keyName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(keyName)); + + bool result = HasSection(sectionName) + && HasSectionKey(sectionName, keyName) + && RemoveValueImpl(sectionName, keyName); + + if (result) + { + SaveConfiguration(); + } + + return result; + } + + /// + public T GetValue(string sectionName, string keyName) + { + if (string.IsNullOrWhiteSpace(sectionName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(sectionName)); + + if (string.IsNullOrWhiteSpace(keyName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(keyName)); + + if (!HasSection(sectionName)) + throw new ConfigurationSectionNotFoundException(sectionName); + + if (!HasSectionKey(sectionName, keyName)) + throw new ConfigurationSectionKeyNotFoundException(sectionName, keyName); + + return GetValueImpl(sectionName, keyName); + } + + /// + public T? GetValueOrDefault(string sectionName, string keyName, T? defaultValue = default) + { + if (string.IsNullOrWhiteSpace(sectionName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(sectionName)); + + if (string.IsNullOrWhiteSpace(keyName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(keyName)); + + if (!HasSection(sectionName)) + return defaultValue; + + if (!HasSectionKey(sectionName, keyName)) + return defaultValue; + + return GetValueImpl(sectionName, keyName); + } + + /// + public void SetValue(string sectionName, string keyName, T? value) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (string.IsNullOrWhiteSpace(sectionName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(sectionName)); + + if (string.IsNullOrWhiteSpace(keyName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(keyName)); + + SetValueImpl(sectionName, keyName, value); + SaveConfiguration(); + } + + protected abstract void SaveConfigurationImpl(); + protected abstract bool HasSectionImpl(string sectionName); + protected abstract bool HasSectionKeyImpl(string sectionName, string keyName); + + protected abstract bool RemoveValueImpl(string sectionName, string keyName); + + protected abstract T GetValueImpl(string sectionName, string keyName); + protected abstract void SetValueImpl(string sectionName, string keyName, T value); +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs new file mode 100644 index 000000000..a0ca7120e --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs @@ -0,0 +1,22 @@ +using pyRevitLabs.Configurations.Abstractions; + +namespace pyRevitLabs.Configurations; + +public sealed class ConfigurationBuilder +{ + private readonly List _configurations = []; + + public ConfigurationBuilder AddConfigurationSource(IConfiguration configuration) + { + if (configuration == null) + throw new ArgumentNullException(nameof(configuration)); + + _configurations.Add(configuration); + return this; + } + + public IConfiguration Build() + { + return ConfigurationService.Create(_configurations); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs new file mode 100644 index 000000000..b6873f92b --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -0,0 +1,95 @@ +using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Exceptions; + +namespace pyRevitLabs.Configurations; + +public sealed class ConfigurationService(List configurations) : IConfiguration +{ + public List Configurations { get; } = configurations; + public IEnumerable ReverseConfigurations => ((IEnumerable)Configurations).Reverse(); + + public static IConfiguration Create(List configurations) + { + return new ConfigurationService(configurations); + } + + public bool HasSection(string sectionName) + { + foreach (IConfiguration configuration in Configurations) + { + if (configuration.HasSection(sectionName)) + { + return true; + } + } + + return false; + } + + public bool HasSectionKey(string sectionName, string keyName) + { + foreach (IConfiguration configuration in ReverseConfigurations) + { + if (configuration.HasSectionKey(sectionName, keyName)) + { + return true; + } + } + + return false; + } + + public T GetValue(string sectionName, string keyName) + { + foreach (IConfiguration configuration in ReverseConfigurations) + { + if (configuration.HasSectionKey(sectionName, keyName)) + { + return configuration.GetValue(sectionName, keyName); + } + } + + throw new ConfigurationException("Section not found"); + } + + public T? GetValueOrDefault(string sectionName, string keyName, T? defaultValue = default) + { + foreach (IConfiguration configuration in ReverseConfigurations) + { + if (configuration.HasSectionKey(sectionName, keyName)) + { + return configuration.GetValueOrDefault(sectionName, keyName, defaultValue); + } + } + + return defaultValue; + } + + public bool RemoveValue(string sectionName, string keyName) + { + foreach (IConfiguration configuration in ReverseConfigurations) + { + if (configuration.HasSectionKey(sectionName, keyName)) + { + return configuration.RemoveValue(sectionName, keyName); + } + } + + return false; + } + + public void SetValue(string sectionName, string keyName, T? value) + { + // default save in main config + foreach (IConfiguration configuration in Configurations) + { + if (configuration.HasSectionKey(sectionName, keyName)) + { + configuration.SetValue(sectionName, keyName, value); + return; + } + } + + Configurations[0].SetValue(sectionName, keyName, value); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationException.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationException.cs new file mode 100644 index 000000000..30658795b --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationException.cs @@ -0,0 +1,10 @@ +namespace pyRevitLabs.Configurations.Exceptions; + +/// +public class ConfigurationException(string message) : Exception(message) +{ + public ConfigurationException() : this("") + { + + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationSectionKeyNotFoundException.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationSectionKeyNotFoundException.cs new file mode 100644 index 000000000..00775453c --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationSectionKeyNotFoundException.cs @@ -0,0 +1,13 @@ +namespace pyRevitLabs.Configurations.Exceptions; + +public sealed class ConfigurationSectionKeyNotFoundException(string message, string keyName, string sectionName) + : ConfigurationException(message) +{ + public string KeyName { get; } = keyName; + public string SectionName { get; } = sectionName; + + public ConfigurationSectionKeyNotFoundException(string keyName, string sectionName) + : this("", keyName, sectionName) + { + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationSectionNotFoundException.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationSectionNotFoundException.cs new file mode 100644 index 000000000..e75c71a64 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Exceptions/ConfigurationSectionNotFoundException.cs @@ -0,0 +1,13 @@ +namespace pyRevitLabs.Configurations.Exceptions; + +public sealed class ConfigurationSectionNotFoundException(string message, string sectionName) + : ConfigurationException(message) +{ + public string SectionName { get; } = sectionName; + + public ConfigurationSectionNotFoundException(string sectionName) + : this("", sectionName) + { + + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Extensions/ConfigurationExtensions.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Extensions/ConfigurationExtensions.cs new file mode 100644 index 000000000..36651e23c --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,6 @@ +namespace pyRevitLabs.Configurations.Extensions; + +public static class ConfigurationExtensions +{ + +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/pyRevitLabs.Configurations.csproj b/dev/pyRevitLabs/pyRevitLabs.Configurations/pyRevitLabs.Configurations.csproj new file mode 100644 index 000000000..849661bbf --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/pyRevitLabs.Configurations.csproj @@ -0,0 +1,10 @@ + + + + net48;net8.0 + enable + enable + 12 + + + diff --git a/dev/pyRevitLabs/pyRevitLabs.sln b/dev/pyRevitLabs/pyRevitLabs.sln index 8a90951a0..55aaeff72 100644 --- a/dev/pyRevitLabs/pyRevitLabs.sln +++ b/dev/pyRevitLabs/pyRevitLabs.sln @@ -51,6 +51,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "pyRevitLabs.UnitTests", "py EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "pyRevitLabs.PyRevit.Runtime.Shared", "pyRevitLabs.PyRevit.Runtime.Shared\pyRevitLabs.PyRevit.Runtime.Shared.csproj", "{B239FF1F-F32A-4828-87B8-E5DAC6809567}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DE5976E3-62D9-4D0E-AC25-37EC56205E91}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitLabs.Configurations.Ini.Tests", "tests\pyRevitLabs.Configurations.Ini.Tests\pyRevitLabs.Configurations.Ini.Tests.csproj", "{314579D4-4AA8-4E67-B56B-D69BC26DD50F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitLabs.Configurations.Json.Tests", "tests\pyRevitLabs.Configurations.Json.Tests\pyRevitLabs.Configurations.Json.Tests.csproj", "{3DC4256D-2003-4905-A3E9-6E03B28476EF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitLabs.Configurations.Tests", "tests\pyRevitLabs.Configurations.Tests\pyRevitLabs.Configurations.Tests.csproj", "{CE552C0D-86B3-47E9-8A6F-8835A60A15B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitLabs.Configurations.Yaml.Tests", "tests\pyRevitLabs.Configurations.Yaml.Tests\pyRevitLabs.Configurations.Yaml.Tests.csproj", "{A9FA927C-8D54-4ED1-B1F7-D736A9089445}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitLabs.Configurations", "pyRevitLabs.Configurations\pyRevitLabs.Configurations.csproj", "{088A33E4-CB8B-4C39-A307-A73E3C93D673}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitLabs.Configurations.Ini", "pyRevitLabs.Configurations.Ini\pyRevitLabs.Configurations.Ini.csproj", "{EB427B0D-FCFD-423D-B95E-CAD0E6BA8532}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitLabs.Configurations.Json", "pyRevitLabs.Configurations.Json\pyRevitLabs.Configurations.Json.csproj", "{BA542A4C-B24B-4794-8D90-936D89B72F31}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitLabs.Configurations.Yaml", "pyRevitLabs.Configurations.Yaml\pyRevitLabs.Configurations.Yaml.csproj", "{E6B0F5EC-1E4E-4BB0-8D9E-640BC3B39A57}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -113,6 +131,38 @@ Global {B239FF1F-F32A-4828-87B8-E5DAC6809567}.Debug|x64.Build.0 = Debug|x64 {B239FF1F-F32A-4828-87B8-E5DAC6809567}.Release|x64.ActiveCfg = Release|x64 {B239FF1F-F32A-4828-87B8-E5DAC6809567}.Release|x64.Build.0 = Release|x64 + {314579D4-4AA8-4E67-B56B-D69BC26DD50F}.Debug|x64.ActiveCfg = Debug|Any CPU + {314579D4-4AA8-4E67-B56B-D69BC26DD50F}.Debug|x64.Build.0 = Debug|Any CPU + {314579D4-4AA8-4E67-B56B-D69BC26DD50F}.Release|x64.ActiveCfg = Release|Any CPU + {314579D4-4AA8-4E67-B56B-D69BC26DD50F}.Release|x64.Build.0 = Release|Any CPU + {3DC4256D-2003-4905-A3E9-6E03B28476EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {3DC4256D-2003-4905-A3E9-6E03B28476EF}.Debug|x64.Build.0 = Debug|Any CPU + {3DC4256D-2003-4905-A3E9-6E03B28476EF}.Release|x64.ActiveCfg = Release|Any CPU + {3DC4256D-2003-4905-A3E9-6E03B28476EF}.Release|x64.Build.0 = Release|Any CPU + {CE552C0D-86B3-47E9-8A6F-8835A60A15B7}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE552C0D-86B3-47E9-8A6F-8835A60A15B7}.Debug|x64.Build.0 = Debug|Any CPU + {CE552C0D-86B3-47E9-8A6F-8835A60A15B7}.Release|x64.ActiveCfg = Release|Any CPU + {CE552C0D-86B3-47E9-8A6F-8835A60A15B7}.Release|x64.Build.0 = Release|Any CPU + {A9FA927C-8D54-4ED1-B1F7-D736A9089445}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9FA927C-8D54-4ED1-B1F7-D736A9089445}.Debug|x64.Build.0 = Debug|Any CPU + {A9FA927C-8D54-4ED1-B1F7-D736A9089445}.Release|x64.ActiveCfg = Release|Any CPU + {A9FA927C-8D54-4ED1-B1F7-D736A9089445}.Release|x64.Build.0 = Release|Any CPU + {088A33E4-CB8B-4C39-A307-A73E3C93D673}.Debug|x64.ActiveCfg = Debug|Any CPU + {088A33E4-CB8B-4C39-A307-A73E3C93D673}.Debug|x64.Build.0 = Debug|Any CPU + {088A33E4-CB8B-4C39-A307-A73E3C93D673}.Release|x64.ActiveCfg = Release|Any CPU + {088A33E4-CB8B-4C39-A307-A73E3C93D673}.Release|x64.Build.0 = Release|Any CPU + {EB427B0D-FCFD-423D-B95E-CAD0E6BA8532}.Debug|x64.ActiveCfg = Debug|Any CPU + {EB427B0D-FCFD-423D-B95E-CAD0E6BA8532}.Debug|x64.Build.0 = Debug|Any CPU + {EB427B0D-FCFD-423D-B95E-CAD0E6BA8532}.Release|x64.ActiveCfg = Release|Any CPU + {EB427B0D-FCFD-423D-B95E-CAD0E6BA8532}.Release|x64.Build.0 = Release|Any CPU + {BA542A4C-B24B-4794-8D90-936D89B72F31}.Debug|x64.ActiveCfg = Debug|Any CPU + {BA542A4C-B24B-4794-8D90-936D89B72F31}.Debug|x64.Build.0 = Debug|Any CPU + {BA542A4C-B24B-4794-8D90-936D89B72F31}.Release|x64.ActiveCfg = Release|Any CPU + {BA542A4C-B24B-4794-8D90-936D89B72F31}.Release|x64.Build.0 = Release|Any CPU + {E6B0F5EC-1E4E-4BB0-8D9E-640BC3B39A57}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6B0F5EC-1E4E-4BB0-8D9E-640BC3B39A57}.Debug|x64.Build.0 = Debug|Any CPU + {E6B0F5EC-1E4E-4BB0-8D9E-640BC3B39A57}.Release|x64.ActiveCfg = Release|Any CPU + {E6B0F5EC-1E4E-4BB0-8D9E-640BC3B39A57}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -120,4 +170,10 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {29169547-9C0C-4620-A37C-77CB6B30F4F0} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {314579D4-4AA8-4E67-B56B-D69BC26DD50F} = {DE5976E3-62D9-4D0E-AC25-37EC56205E91} + {3DC4256D-2003-4905-A3E9-6E03B28476EF} = {DE5976E3-62D9-4D0E-AC25-37EC56205E91} + {CE552C0D-86B3-47E9-8A6F-8835A60A15B7} = {DE5976E3-62D9-4D0E-AC25-37EC56205E91} + {A9FA927C-8D54-4ED1-B1F7-D736A9089445} = {DE5976E3-62D9-4D0E-AC25-37EC56205E91} + EndGlobalSection EndGlobal diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs new file mode 100644 index 000000000..47553fa7e --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs @@ -0,0 +1,60 @@ +using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Ini.Extensions; +using pyRevitLabs.Configurations.Tests; + +namespace pyRevitLabs.Configurations.Ini.Tests; + +public class IniConfigurationUnitTests : ConfigurationTests, IClassFixture +{ + private readonly string _configPath; + + public IniConfigurationUnitTests(IniCreateFixture iniCreateFixture) + : base(iniCreateFixture.Configuration) + { + _configPath = IniCreateFixture.ConfigPath; + } + + [Fact] + public void CreateIniConfiguration_ShouldCreate() + { + Assert.NotNull(IniConfiguration.Create(_configPath)); + } + + [Fact] + public void CreateIniConfiguration_ShouldThrowsException() + { + Assert.Throws(() => IniConfiguration.Create(default!)); + } + + [Fact] + public void CreateIniConfigurationByBuilder_ShouldCreate() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddIniConfiguration(_configPath) + .Build(); + + Assert.NotNull(configuration); + } + + [Fact] + public void CreateIniConfigurationByBuilder_ShouldThrowsException() + { + Assert.Throws(() => + { + new ConfigurationBuilder() + .AddIniConfiguration(default!) + .Build(); + }); + } + + [Fact] + public void CreateIniConfigurationByNullBuilder_ShouldThrowsException() + { + Assert.Throws(() => + { + IniConfigurationExtensions + .AddIniConfiguration(default!, default!) + .Build(); + }); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniCreateFixture.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniCreateFixture.cs new file mode 100644 index 000000000..997e3d4a9 --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniCreateFixture.cs @@ -0,0 +1,21 @@ +using pyRevitLabs.Configurations.Abstractions; + +namespace pyRevitLabs.Configurations.Ini.Tests +{ + public sealed class IniCreateFixture : IDisposable + { + public const string ConfigPath = "pyRevit_config.ini"; + + public IniCreateFixture() + { + Configuration = IniConfiguration.Create(ConfigPath); + } + + public IConfiguration Configuration { get; } + + public void Dispose() + { + File.Delete(ConfigPath); + } + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/pyRevitLabs.Configurations.Ini.Tests.csproj b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/pyRevitLabs.Configurations.Ini.Tests.csproj new file mode 100644 index 000000000..9e942d8eb --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/pyRevitLabs.Configurations.Ini.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs new file mode 100644 index 000000000..85db53412 --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs @@ -0,0 +1,60 @@ +using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Json.Extensions; +using pyRevitLabs.Configurations.Tests; + +namespace pyRevitLabs.Configurations.Json.Tests; + +public class JsonConfigurationUnitTests : ConfigurationTests, IClassFixture +{ + private readonly string _configPath; + + public JsonConfigurationUnitTests(JsonCreateFixture createFixture) + : base(createFixture.Configuration) + { + _configPath = JsonCreateFixture.ConfigPath; + } + + [Fact] + public void CreateIniConfiguration_ShouldCreate() + { + Assert.NotNull(JsonConfiguration.Create(_configPath)); + } + + [Fact] + public void CreateIniConfiguration_ShouldThrowsException() + { + Assert.Throws(() => JsonConfiguration.Create(default!)); + } + + [Fact] + public void CreateIniConfigurationByBuilder_ShouldCreate() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonConfiguration(_configPath) + .Build(); + + Assert.NotNull(configuration); + } + + [Fact] + public void CreateIniConfigurationByBuilder_ShouldThrowsException() + { + Assert.Throws(() => + { + new ConfigurationBuilder() + .AddJsonConfiguration(default!) + .Build(); + }); + } + + [Fact] + public void CreateIniConfigurationByNullBuilder_ShouldThrowsException() + { + Assert.Throws(() => + { + JsonConfigurationExtensions + .AddJsonConfiguration(default!, default!) + .Build(); + }); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonCreateFixture.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonCreateFixture.cs new file mode 100644 index 000000000..be14f78b3 --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonCreateFixture.cs @@ -0,0 +1,21 @@ +using pyRevitLabs.Configurations.Abstractions; + +namespace pyRevitLabs.Configurations.Json.Tests +{ + public sealed class JsonCreateFixture : IDisposable + { + public const string ConfigPath = "pyRevit_config.json"; + + public JsonCreateFixture() + { + Configuration = JsonConfiguration.Create(ConfigPath); + } + + public IConfiguration Configuration { get; } + + public void Dispose() + { + File.Delete(ConfigPath); + } + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/pyRevitLabs.Configurations.Json.Tests.csproj b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/pyRevitLabs.Configurations.Json.Tests.csproj new file mode 100644 index 000000000..546ef9ded --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/pyRevitLabs.Configurations.Json.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs new file mode 100644 index 000000000..3bf224a9b --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs @@ -0,0 +1,50 @@ +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; +using pyRevitLabs.Configurations.Abstractions; + +namespace pyRevitLabs.Configurations.Tests +{ + public sealed class ConfigurationServiceFixture : IDisposable + { + public ConfigurationServiceFixture() + { + Configuration = new ConfigurationService(new List() {new TestRunConfiguration()}); + } + + public IConfiguration Configuration { get; } + + public void Dispose() { } + + private class TestRunConfiguration : IConfiguration + { + public bool HasSection(string sectionName) + { + throw new NotImplementedException(); + } + + public bool HasSectionKey(string sectionName, string keyName) + { + throw new NotImplementedException(); + } + + public T GetValue(string sectionName, string keyName) + { + throw new NotImplementedException(); + } + + public T? GetValueOrDefault(string sectionName, string keyName, T? defaultValue = default) + { + throw new NotImplementedException(); + } + + public bool RemoveValue(string sectionName, string keyName) + { + throw new NotImplementedException(); + } + + public void SetValue(string sectionName, string keyName, T? value) + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceUnitTests.cs new file mode 100644 index 000000000..e6369cff1 --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceUnitTests.cs @@ -0,0 +1,12 @@ +using pyRevitLabs.Configurations.Abstractions; + +namespace pyRevitLabs.Configurations.Tests +{ + public sealed class ConfigurationServiceUnitTests : ConfigurationTests, IClassFixture + { + public ConfigurationServiceUnitTests(ConfigurationServiceFixture configurationServiceFixture) + : base(configurationServiceFixture.Configuration) + { + } + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationTests.cs new file mode 100644 index 000000000..d5755fa4b --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationTests.cs @@ -0,0 +1,91 @@ +using pyRevitLabs.Configurations.Abstractions; + +namespace pyRevitLabs.Configurations.Tests; + +public abstract class ConfigurationTests +{ + protected readonly IConfiguration _configuration; + + public ConfigurationTests(IConfiguration configuration) + { + _configuration = configuration; + StartUp(); + } + + private void StartUp() + { + // core + _configuration.SetValue("core", "userextensions", new List()); + _configuration.SetValue("core", "user_locale", "ru"); + _configuration.SetValue("core", "rocketmode", true); + _configuration.SetValue("core", "autoupdate", true); + _configuration.SetValue("core", "checkupdates", true); + _configuration.SetValue("core", "usercanextend", true); + _configuration.SetValue("core", "usercanconfig", true); + + // environment + _configuration.SetValue("environment", "clones", + new Dictionary() {{"master", "C:\\Users\\user\\AppData\\Roaming\\pyRevit-Master"}}); + + // pyRevitBundlesCreatorExtension.extension + _configuration.SetValue("pyRevitBundlesCreatorExtension.extension", "disabled", true); + _configuration.SetValue("pyRevitBundlesCreatorExtension.extension", "private_repo", true); + _configuration.SetValue("pyRevitBundlesCreatorExtension.extension", "username", ""); + _configuration.SetValue("pyRevitBundlesCreatorExtension.extension", "password", ""); + + // telemetry + _configuration.SetValue("telemetry", "active", true); + _configuration.SetValue("telemetry", "utc_timestamps", true); + _configuration.SetValue("telemetry", "active_app", true); + _configuration.SetValue("telemetry", "apptelemetry_event_flags", "0x4000400004003"); + _configuration.SetValue("telemetry", "apptelemetry_event_flags", "0x4000400004003"); + _configuration.SetValue("telemetry", "telemetry_server_url", "http://pyrevitlabs.io/api/v2/scripts"); + _configuration.SetValue("telemetry", "apptelemetry_server_url", "http://pyrevitlabs/api/v2/events"); + } + + [Fact] + public void NewCreateValue_ShouldReturnsSameValue() + { + _configuration.SetValue("new", "create", "value"); + + string value = _configuration.GetValue("new", "create"); + + Assert.Equal("value", value); + } + + [Fact] + public void GetValueOrDefault_ShouldReturnsSameValue() + { + _configuration.SetValue("create", "default", "value"); + + string? value = _configuration.GetValueOrDefault("create", "default"); + + Assert.Equal("value", value); + } + + [Fact] + public void GetValueOrDefault_ShouldReturnsDefaultValue() + { + string? value = _configuration.GetValueOrDefault("not_exits", "key", "defaultValue"); + + Assert.Equal("defaultValue", value); + } + + [Fact] + public void RemoveExitsValue_ShouldReturnsTrue() + { + _configuration.SetValue("remove", "default", "value"); + + bool result = _configuration.RemoveValue("remove", "default"); + + Assert.True(result); + } + + [Fact] + public void RemoveNotExitsValue_ShouldReturnFalse() + { + bool result = _configuration.RemoveValue("remove", "not-exits"); + + Assert.False(result); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/pyRevitLabs.Configurations.Tests.csproj b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/pyRevitLabs.Configurations.Tests.csproj new file mode 100644 index 000000000..53c64ce05 --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/pyRevitLabs.Configurations.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs new file mode 100644 index 000000000..cfa23a9dc --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs @@ -0,0 +1,60 @@ +using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Tests; +using pyRevitLabs.Configurations.Yaml.Extensions; + +namespace pyRevitLabs.Configurations.Yaml.Tests; + +public class YamlConfigurationUnitTests : ConfigurationTests, IClassFixture +{ + private readonly string _configPath; + + public YamlConfigurationUnitTests(YamlCreateFixture createFixture) + : base(createFixture.Configuration) + { + _configPath = YamlCreateFixture.ConfigPath; + } + + [Fact] + public void CreateIniConfiguration_ShouldCreate() + { + Assert.NotNull(YamlConfiguration.Create(_configPath)); + } + + [Fact] + public void CreateIniConfiguration_ShouldThrowsException() + { + Assert.Throws(() => YamlConfiguration.Create(default!)); + } + + [Fact] + public void CreateIniConfigurationByBuilder_ShouldCreate() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddYamlConfiguration(_configPath) + .Build(); + + Assert.NotNull(configuration); + } + + [Fact] + public void CreateIniConfigurationByBuilder_ShouldThrowsException() + { + Assert.Throws(() => + { + new ConfigurationBuilder() + .AddYamlConfiguration(default!) + .Build(); + }); + } + + [Fact] + public void CreateIniConfigurationByNullBuilder_ShouldThrowsException() + { + Assert.Throws(() => + { + YamlConfigurationExtensions + .AddYamlConfiguration(default!, default!) + .Build(); + }); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlCreateFixture.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlCreateFixture.cs new file mode 100644 index 000000000..592d31c20 --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlCreateFixture.cs @@ -0,0 +1,21 @@ +using pyRevitLabs.Configurations.Abstractions; + +namespace pyRevitLabs.Configurations.Yaml.Tests +{ + public sealed class YamlCreateFixture : IDisposable + { + public const string ConfigPath = "pyRevit_config.yml"; + + public YamlCreateFixture() + { + Configuration = YamlConfiguration.Create(ConfigPath); + } + + public IConfiguration Configuration { get; } + + public void Dispose() + { + File.Delete(ConfigPath); + } + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj new file mode 100644 index 000000000..83d8d629e --- /dev/null +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + From 492e6e3219948e3e8f6c028703c8aa0cb2f22e2f Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 15:19:22 +0600 Subject: [PATCH 02/37] add ini project --- .../Extensions/IniConfigurationExtensions.cs | 15 +++ .../IniConfiguration.cs | 93 +++++++++++++++++++ .../pyRevitLabs.Configurations.Ini.csproj | 19 ++++ 3 files changed, 127 insertions(+) create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/Extensions/IniConfigurationExtensions.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/pyRevitLabs.Configurations.Ini.csproj diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/Extensions/IniConfigurationExtensions.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/Extensions/IniConfigurationExtensions.cs new file mode 100644 index 000000000..cae28966c --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/Extensions/IniConfigurationExtensions.cs @@ -0,0 +1,15 @@ +namespace pyRevitLabs.Configurations.Ini.Extensions; + +public static class IniConfigurationExtensions +{ + public static ConfigurationBuilder AddIniConfiguration(this ConfigurationBuilder builder, string configurationPath, bool readOnly = default) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); + + if (string.IsNullOrWhiteSpace(configurationPath)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(configurationPath)); + + return builder.AddConfigurationSource(IniConfiguration.Create(configurationPath, readOnly)); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs new file mode 100644 index 000000000..c32979412 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs @@ -0,0 +1,93 @@ +using System.Text; +using IniParser; +using IniParser.Model; +using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Exceptions; +using pyRevitLabs.Json; + +namespace pyRevitLabs.Configurations.Ini; + +public sealed class IniConfiguration : ConfigurationBase +{ + public static readonly Encoding DefaultIniFileEncoding = new UTF8Encoding(false); + + private readonly IniData _iniFile; + private readonly FileIniDataParser _parser; + + /// + /// Create ini configuration instance. + /// + /// Configuration path. + /// Admin configurations + private IniConfiguration(string configurationPath, bool readOnly) + : base(configurationPath, readOnly) + { + _parser = new FileIniDataParser(); + _iniFile = !File.Exists(configurationPath) + ? new IniData() + : _parser.ReadFile(_configurationPath, DefaultIniFileEncoding); + } + + /// + /// Creates IniConfiguration. + /// + /// Configuration file path. + /// Mark file is readonly. + /// Return new IniConfiguration. + /// When configurationPath is null. + public static IConfiguration Create(string configurationPath, bool readOnly = default) + { + if (configurationPath is null) + throw new ArgumentNullException(nameof(configurationPath)); + + return new IniConfiguration(configurationPath, readOnly); + } + + /// + protected override void SaveConfigurationImpl() + { + _parser.WriteFile(_configurationPath, _iniFile, DefaultIniFileEncoding); + } + + /// + protected override bool HasSectionImpl(string sectionName) + { + return _iniFile.Sections.ContainsSection(sectionName); + } + + /// + protected override bool HasSectionKeyImpl(string sectionName, string keyName) + { + return HasSection(sectionName) + && _iniFile.Sections[sectionName].ContainsKey(keyName); + } + + /// + protected override bool RemoveValueImpl(string sectionName, string keyName) + { + return _iniFile[sectionName].RemoveKey(keyName); + } + + /// + protected override T GetValueImpl(string sectionName, string keyName) + { + return JsonConvert.DeserializeObject(_iniFile[sectionName][keyName]) + ?? throw new ConfigurationException("Cannot deserialize value using the specified key."); + } + + /// + protected override void SetValueImpl(string sectionName, string keyName, T value) + { + if (!HasSection(sectionName)) + { + _iniFile.Sections.AddSection(sectionName); + } + + if (!HasSectionKey(sectionName, keyName)) + { + _iniFile[sectionName].AddKey(keyName); + } + + _iniFile[sectionName][keyName] = JsonConvert.SerializeObject(value); + } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/pyRevitLabs.Configurations.Ini.csproj b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/pyRevitLabs.Configurations.Ini.csproj new file mode 100644 index 000000000..baf5f7f01 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/pyRevitLabs.Configurations.Ini.csproj @@ -0,0 +1,19 @@ + + + + net48;net8.0 + enable + enable + 12 + + + + + + + + + + + + From 98c40b3f4c74f3f7e894f15516af2c1a5abacc36 Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 12:32:11 +0300 Subject: [PATCH 03/37] fix build --- dev/Directory.Build.targets | 2 +- .../pyRevitLabs.Configurations.Ini.Tests.csproj | 4 ++-- .../pyRevitLabs.Configurations.Json.Tests.csproj | 4 ++-- .../pyRevitLabs.Configurations.Tests.csproj | 2 +- .../pyRevitLabs.Configurations.Yaml.Tests.csproj | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dev/Directory.Build.targets b/dev/Directory.Build.targets index e7226e7b4..9bbdad2c0 100644 --- a/dev/Directory.Build.targets +++ b/dev/Directory.Build.targets @@ -2,7 +2,7 @@ netfx - netcore + netcore diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/pyRevitLabs.Configurations.Ini.Tests.csproj b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/pyRevitLabs.Configurations.Ini.Tests.csproj index 9e942d8eb..535928273 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/pyRevitLabs.Configurations.Ini.Tests.csproj +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/pyRevitLabs.Configurations.Ini.Tests.csproj @@ -21,8 +21,8 @@ - - + + diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/pyRevitLabs.Configurations.Json.Tests.csproj b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/pyRevitLabs.Configurations.Json.Tests.csproj index 546ef9ded..fd4377eee 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/pyRevitLabs.Configurations.Json.Tests.csproj +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/pyRevitLabs.Configurations.Json.Tests.csproj @@ -21,8 +21,8 @@ - - + + diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/pyRevitLabs.Configurations.Tests.csproj b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/pyRevitLabs.Configurations.Tests.csproj index 53c64ce05..a04d5ec92 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/pyRevitLabs.Configurations.Tests.csproj +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/pyRevitLabs.Configurations.Tests.csproj @@ -21,7 +21,7 @@ - + diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj index 83d8d629e..4698eb69e 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj @@ -21,8 +21,8 @@ - - + + From d9c2251386d4c4eeb8ac9f7413901b6e9ee12ef4 Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 12:45:19 +0300 Subject: [PATCH 04/37] remove unnecessary usings --- .../pyRevitLabs.PyRevit/PyRevitAttachments.cs | 6 ++---- .../pyRevitLabs.PyRevit/PyRevitCaches.cs | 14 ++------------ .../pyRevitLabs.PyRevit/PyRevitClones.cs | 4 +--- .../pyRevitLabs.PyRevit/PyRevitExtensions.cs | 4 +--- .../pyRevitLabs.PyRevit/pyRevitLabs.PyRevit.csproj | 6 +++--- 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitAttachments.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitAttachments.cs index a51339782..c3f0d5771 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitAttachments.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitAttachments.cs @@ -2,15 +2,13 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; -using System.Text.RegularExpressions; +using System.Linq; using System.Security.Principal; using System.Text; -using System.Linq; +using System.Text.RegularExpressions; using pyRevitLabs.Common; using pyRevitLabs.Common.Extensions; - -using MadMilkman.Ini; using pyRevitLabs.Json.Linq; using pyRevitLabs.NLog; using pyRevitLabs.TargetApps.Revit; diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitCaches.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitCaches.cs index c91f20dfe..e40f4714d 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitCaches.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitCaches.cs @@ -1,22 +1,12 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; +using System.IO; using System.Text.RegularExpressions; -using System.Security.Principal; -using System.Text; - using pyRevitLabs.Common; -using pyRevitLabs.Common.Extensions; - -using MadMilkman.Ini; -using pyRevitLabs.Json.Linq; using pyRevitLabs.NLog; using pyRevitLabs.TargetApps.Revit; namespace pyRevitLabs.PyRevit { public static class PyRevitCaches { - private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); // pyrevit cache folder // @reviewed diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitClones.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitClones.cs index 8d58d8028..b317233a1 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitClones.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitClones.cs @@ -2,14 +2,12 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; -using System.Text.RegularExpressions; using System.Security.Principal; using System.Text; +using System.Text.RegularExpressions; using pyRevitLabs.Common; using pyRevitLabs.Common.Extensions; - -using MadMilkman.Ini; using pyRevitLabs.Json.Linq; using pyRevitLabs.NLog; diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs index 3e059c4ad..a8ad2ba87 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs @@ -2,14 +2,12 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; -using System.Text.RegularExpressions; using System.Security.Principal; using System.Text; +using System.Text.RegularExpressions; using pyRevitLabs.Common; using pyRevitLabs.Common.Extensions; - -using MadMilkman.Ini; using pyRevitLabs.Json.Linq; using pyRevitLabs.NLog; diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/pyRevitLabs.PyRevit.csproj b/dev/pyRevitLabs/pyRevitLabs.PyRevit/pyRevitLabs.PyRevit.csproj index 5370f8149..f7617ba10 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/pyRevitLabs.PyRevit.csproj +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/pyRevitLabs.PyRevit.csproj @@ -17,14 +17,14 @@ - - - + + + From 63393d72871f89c59c69c9100ba3d052a3c3de2a Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 13:12:58 +0300 Subject: [PATCH 05/37] update create config --- .../pyRevitLabs.PyRevit/PyRevitConfigs.cs | 160 +++++++++--------- 1 file changed, 76 insertions(+), 84 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs index 3e03db265..6a986e6c1 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs @@ -4,30 +4,13 @@ using System.Security.AccessControl; using pyRevitLabs.Common; +using pyRevitLabs.Configurations; +using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Ini.Extensions; using pyRevitLabs.NLog; namespace pyRevitLabs.PyRevit { - public class PyRevitConfigValueNotSet : PyRevitException - { - public PyRevitConfigValueNotSet(string sectionName, string keyName) - { - ConfigSection = sectionName; - ConfigKey = keyName; - } - - public string ConfigSection { get; set; } - public string ConfigKey { get; set; } - - public override string Message - { - get - { - return String.Format("Config value not set \"{0}:{1}\"", ConfigSection, ConfigKey); - } - } - } - public enum PyRevitLogLevels { Quiet, @@ -37,43 +20,47 @@ public enum PyRevitLogLevels public static class PyRevitConfigs { - private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); - // get config file + /// + /// Returns config file. + /// + /// Returns admin config if admin config exists and user config not found. public static PyRevitConfig GetConfigFile() { // make sure the file exists and if not create an empty one string userConfig = PyRevitConsts.ConfigFilePath; string adminConfig = PyRevitConsts.AdminConfigFilePath; - if (!CommonUtils.VerifyFile(userConfig)) + if (!File.Exists(userConfig) + && File.Exists(adminConfig)) { - if (CommonUtils.VerifyFile(adminConfig)) - { - if (new FileInfo(adminConfig).IsReadOnly) - return new PyRevitConfig(adminConfig, adminMode: true); - else - SetupConfig(adminConfig); - } - else - SetupConfig(); + _logger.Info("Creating admin config {@ConfigPath}", adminConfig); + return CreateConfiguration(adminConfig); } - return new PyRevitConfig(userConfig); + _logger.Info("Creating user config {@ConfigPath}", userConfig); + return CreateConfiguration(userConfig); } - // deletes config file + /// + /// Removes user config file. + /// + /// public static void DeleteConfig() { - if (File.Exists(PyRevitConsts.ConfigFilePath)) - try - { - File.Delete(PyRevitConsts.ConfigFilePath); - } - catch (Exception ex) - { - throw new PyRevitException(string.Format("Failed deleting config file \"{0}\" | {1}", PyRevitConsts.ConfigFilePath, ex.Message)); - } + if (!File.Exists(PyRevitConsts.ConfigFilePath)) return; + + _logger.Info("Deleting config {@ConfigPath}", PyRevitConsts.ConfigFilePath); + + try + { + File.Delete(PyRevitConsts.ConfigFilePath); + } + catch (Exception ex) + { + throw new PyRevitException($"Failed deleting config file \"{PyRevitConsts.ConfigFilePath}\"", ex); + } } // copy config file into all users directory as seed config file @@ -82,37 +69,33 @@ public static void SeedConfig(bool lockSeedConfig = false) string sourceFile = PyRevitConsts.ConfigFilePath; string targetFile = PyRevitConsts.AdminConfigFilePath; - logger.Debug("Seeding config file \"{0}\" to \"{1}\"", sourceFile, targetFile); + _logger.Debug("Seeding config file \"{@SourceFile}\" to \"{@TargetFile}\"", sourceFile, targetFile); + + if (!File.Exists(sourceFile)) return; try { - if (File.Exists(sourceFile)) - { - File.Copy(sourceFile, targetFile, true); + File.Copy(sourceFile, targetFile, true); - if (lockSeedConfig) + if (lockSeedConfig) + { + try + { + File.SetAttributes(targetFile, FileAttributes.ReadOnly); + } + catch (InvalidOperationException ex) { var currentUser = WindowsIdentity.GetCurrent(); - try - { - File.SetAttributes(targetFile, FileAttributes.ReadOnly); - } - catch (InvalidOperationException ex) - { - logger.Error( - string.Format( - "You cannot assign ownership to user \"{0}\"." + - "Either you don't have TakeOwnership permissions, " + - "or it is not your user account. | {1}", currentUser.Name, ex.Message - ) - ); - } + _logger.Error(ex, + $"You cannot assign ownership to user \"{currentUser.Name}\"." + + "Either you don't have TakeOwnership permissions, " + + $"or it is not your user account."); } } } catch (Exception ex) { - throw new PyRevitException(string.Format("Failed seeding config file. | {0}", ex.Message)); + throw new PyRevitException("Failed seeding config file.", ex); } } @@ -123,23 +106,32 @@ public static void SetupConfig(string templateConfigFilePath = null) string sourceFile = templateConfigFilePath; string targetFile = PyRevitConsts.ConfigFilePath; - if (sourceFile is string) + if (string.IsNullOrEmpty(sourceFile)) { - logger.Debug("Seeding config file \"{0}\" to \"{1}\"", sourceFile, targetFile); + CommonUtils.EnsureFile(targetFile); + return; + } - try - { - File.WriteAllText(targetFile, File.ReadAllText(sourceFile)); - } - catch (Exception ex) - { - throw new PyRevitException( - $"Failed configuring config file from template at {sourceFile} | {ex.Message}" - ); - } + + _logger.Debug("Seeding config file \"{@SourceFile}\" to \"{@SargetFile}\"", sourceFile, targetFile); + + try + { + File.WriteAllText(targetFile, File.ReadAllText(sourceFile)); } - else - CommonUtils.EnsureFile(targetFile); + catch (Exception ex) + { + throw new PyRevitException($"Failed configuring config file from template at {sourceFile}", ex); + } + } + + private static PyRevitConfig CreateConfiguration(string configPath) + { + var configuration = new ConfigurationBuilder() + .AddIniConfiguration(configPath) + .Build(); + + return new PyRevitConfig(configuration); } // specific configuration public access ====================================================================== @@ -154,7 +146,7 @@ public static bool GetUTCStamps() public static void SetUTCStamps(bool state) { var cfg = GetConfigFile(); - logger.Debug("Setting telemetry utc timestamps..."); + _logger.Debug("Setting telemetry utc timestamps..."); cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryUTCTimestampsKey, state); } @@ -243,7 +235,7 @@ public static string GetTelemetryServerUrl() public static void EnableTelemetry(string telemetryFileDir = null, string telemetryServerUrl = null) { var cfg = GetConfigFile(); - logger.Debug(string.Format("Enabling telemetry... path: \"{0}\" server: {1}", + _logger.Debug(string.Format("Enabling telemetry... path: \"{0}\" server: {1}", telemetryFileDir, telemetryServerUrl)); SetTelemetryStatus(true); @@ -259,7 +251,7 @@ public static void EnableTelemetry(string telemetryFileDir = null, string teleme if (CommonUtils.VerifyPath(telemetryFileDir)) cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryFileDirKey, telemetryFileDir); else - logger.Debug("Invalid log path \"{0}\"", telemetryFileDir); + _logger.Debug("Invalid log path \"{0}\"", telemetryFileDir); } } @@ -283,7 +275,7 @@ public static void SetTelemetryIncludeHooks(bool state) public static void DisableTelemetry() { var cfg = GetConfigFile(); - logger.Debug("Disabling telemetry..."); + _logger.Debug("Disabling telemetry..."); cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryStatusKey, false); } @@ -310,7 +302,7 @@ public static string GetAppTelemetryServerUrl() public static void EnableAppTelemetry(string apptelemetryServerUrl = null) { var cfg = GetConfigFile(); - logger.Debug(string.Format("Enabling app telemetry... server: {0}", apptelemetryServerUrl)); + _logger.Debug(string.Format("Enabling app telemetry... server: {0}", apptelemetryServerUrl)); SetAppTelemetryStatus(true); if (apptelemetryServerUrl != null) @@ -320,7 +312,7 @@ public static void EnableAppTelemetry(string apptelemetryServerUrl = null) public static void DisableAppTelemetry() { var cfg = GetConfigFile(); - logger.Debug("Disabling app telemetry..."); + _logger.Debug("Disabling app telemetry..."); cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsAppTelemetryStatusKey, false); } @@ -333,7 +325,7 @@ public static string GetAppTelemetryFlags() public static void SetAppTelemetryFlags(string flags) { var cfg = GetConfigFile(); - logger.Debug("Setting app telemetry flags..."); + _logger.Debug("Setting app telemetry flags..."); if (flags != null) cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsAppTelemetryEventFlagsKey, flags); } From 2afea49e9ef24255670a6e1a8411737df8da8d67 Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 13:21:53 +0300 Subject: [PATCH 06/37] rewrite config class to new configs --- .../pyRevitLabs.PyRevit/PyRevitConfig.cs | 209 ++++++++---------- 1 file changed, 87 insertions(+), 122 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs index 2d447d55e..da5c74c9d 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs @@ -1,147 +1,112 @@ using System; using System.Collections.Generic; +using System.Linq; -using pyRevitLabs.Common; -using pyRevitLabs.Common.Extensions; - -using MadMilkman.Ini; +using pyRevitLabs.Configurations.Abstractions; using pyRevitLabs.NLog; -namespace pyRevitLabs.PyRevit { - public class PyRevitConfig { - private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - - private IniFile _config; - private bool _adminMode; - - public string ConfigFilePath { get; private set; } - - public PyRevitConfig(string cfgFilePath, bool adminMode = false) { - if (cfgFilePath is null) - throw new PyRevitException("Config file path can not be null."); +namespace pyRevitLabs.PyRevit +{ + public class PyRevitConfig + { + private readonly IConfiguration _configuration; + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); - if (CommonUtils.VerifyFile(cfgFilePath)) - { - ConfigFilePath = cfgFilePath; - - // INI formatting - var cfgOps = new IniOptions(); - cfgOps.KeySpaceAroundDelimiter = true; - cfgOps.Encoding = CommonUtils.GetUTF8NoBOMEncoding(); - _config = new IniFile(cfgOps); - - _config.Load(cfgFilePath); - _adminMode = adminMode; - } - else - throw new PyRevitException($"Can not access config file at {cfgFilePath}"); + public PyRevitConfig(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } - // save config file to standard location - public void SaveConfigFile() { - if (_adminMode) { - logger.Debug("Config is in admin mode. Skipping save"); - return; - } - - logger.Debug("Saving config file \"{0}\"", PyRevitConsts.ConfigFilePath); - try { - _config.Save(PyRevitConsts.ConfigFilePath); - } - catch (Exception ex) { - throw new PyRevitException(string.Format("Failed to save config to \"{0}\". | {1}", - PyRevitConsts.ConfigFilePath, ex.Message)); - } + /// + /// Get config key value + /// + /// + /// + /// + public string GetValue(string sectionName, string keyName) + { + _logger.Debug("Try getting config value \"{@SectionName}:{@KeyName}\"", sectionName, keyName); + return _configuration.GetValue(sectionName, keyName); } - // get config key value - public string GetValue(string sectionName, string keyName) { - logger.Debug(string.Format("Try getting config \"{0}:{1}\"", sectionName, keyName)); - if (_config.Sections.Contains(sectionName) && _config.Sections[sectionName].Keys.Contains(keyName)) { - var cfgValue = _config.Sections[sectionName].Keys[keyName].Value as string; - logger.Debug(string.Format("Config \"{0}:{1}\" = \"{2}\"", sectionName, keyName, cfgValue)); - return cfgValue; - } - else { - logger.Debug(string.Format("Config \"{0}:{1}\" not set.", sectionName, keyName)); - return null; - } + /// + /// Get config key value and make a string list out of it + /// + /// + /// + /// + public List GetListValue(string sectionName, string keyName) + { + _logger.Debug("Try getting config as list value \"{SectionName}:{KeyName}\"", sectionName, keyName); + return _configuration.GetValue>(sectionName, keyName); } - // get config key value and make a string list out of it - public List GetListValue(string sectionName, string keyName) { - logger.Debug("Try getting config as list \"{0}:{1}\"", sectionName, keyName); - var stringValue = GetValue(sectionName, keyName); - if (stringValue != null) - return stringValue.ConvertFromTomlListString(); - else - return null; + /// + /// Get config key value and make a string dictionary out of it + /// + /// + /// + /// + public Dictionary GetDictValue(string sectionName, string keyName) + { + _logger.Debug("Try getting config as dict value \"{SectionName}:{KeyName}\"", sectionName, keyName); + return _configuration.GetValue>(sectionName, keyName); } - // get config key value and make a string dictionary out of it - public Dictionary GetDictValue(string sectionName, string keyName) { - logger.Debug("Try getting config as dict \"{0}:{1}\"", sectionName, keyName); - var stringValue = GetValue(sectionName, keyName); - if (stringValue != null) - return stringValue.ConvertFromTomlDictString(); - else - return null; + /// + /// Set config key value, creates the config if not set yet + /// + /// + /// + /// + public void SetValue(string sectionName, string keyName, string value) + { + _configuration.SetValue(sectionName, keyName, value); } - // set config key value, creates the config if not set yet - public void SetValue(string sectionName, string keyName, string stringValue) { - if (stringValue != null) { - if (!_config.Sections.Contains(sectionName)) { - logger.Debug("Adding config section \"{0}\"", sectionName); - _config.Sections.Add(sectionName); - } - - if (!_config.Sections[sectionName].Keys.Contains(keyName)) { - logger.Debug("Adding config key \"{0}:{1}\"", sectionName, keyName); - _config.Sections[sectionName].Keys.Add(keyName); - } - - logger.Debug("Updating config \"{0}:{1} = {2}\"", sectionName, keyName, stringValue); - _config.Sections[sectionName].Keys[keyName].Value = stringValue; - - SaveConfigFile(); - } - else - logger.Debug("Can not set null value for \"{0}:{1}\"", sectionName, keyName); + /// + /// Sets config key value as bool + /// + /// + /// + /// + public void SetValue(string sectionName, string keyName, bool value) + { + _configuration.SetValue(sectionName, keyName, value); } - // sets config key value as bool - public void SetValue(string sectionName, string keyName, bool boolValue) { - SetValue(sectionName, keyName, boolValue.ConvertToTomlBoolString()); + /// + /// Sets config key value as int + /// + /// + /// + /// + public void SetValue(string sectionName, string keyName, int value) + { + _configuration.SetValue(sectionName, keyName, value); } - // sets config key value as int - public void SetValue(string sectionName, string keyName, int intValue) { - SetValue(sectionName, keyName, intValue.ConvertToTomlIntString()); + /// + /// Sets config key value as string list + /// + /// + /// + /// + public void SetValue(string sectionName, string keyName, IEnumerable value) + { + _configuration.SetValue(sectionName, keyName, value.ToList()); } - // sets config key value as string list - public void SetValue(string sectionName, string keyName, IEnumerable listString) { - SetValue(sectionName, keyName, listString.ConvertToTomlListString()); - } - - // sets config key value as string dictionary - public void SetValue(string sectionName, string keyName, IDictionary dictString) { - SetValue(sectionName, keyName, dictString.ConvertToTomlDictString()); - } - - // removes a value from config file - public bool DeleteValue(string sectionName, string keyName) { - logger.Debug(string.Format("Try getting config \"{0}:{1}\"", sectionName, keyName)); - if (_config.Sections.Contains(sectionName) && _config.Sections[sectionName].Keys.Contains(keyName)) { - logger.Debug(string.Format("Removing config \"{0}:{1}\"", sectionName, keyName)); - return _config.Sections[sectionName].Keys.Remove(keyName); - } - else { - logger.Debug(string.Format("Config \"{0}:{1}\" not set.", sectionName, keyName)); - return false; - } + /// + /// Sets config key value as string dictionary + /// + /// + /// + /// + public void SetValue(string sectionName, string keyName, IDictionary dictString) + { + _configuration.SetValue(sectionName, keyName, dictString); } } -} +} \ No newline at end of file From 68efd976efdd44b736a83b387c06ea073dcf84d9 Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 15:22:20 +0300 Subject: [PATCH 07/37] change return default value for lists --- dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs index da5c74c9d..7b44c4f08 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs @@ -27,7 +27,7 @@ public PyRevitConfig(IConfiguration configuration) public string GetValue(string sectionName, string keyName) { _logger.Debug("Try getting config value \"{@SectionName}:{@KeyName}\"", sectionName, keyName); - return _configuration.GetValue(sectionName, keyName); + return _configuration.GetValueOrDefault(sectionName, keyName); } /// @@ -39,7 +39,7 @@ public string GetValue(string sectionName, string keyName) public List GetListValue(string sectionName, string keyName) { _logger.Debug("Try getting config as list value \"{SectionName}:{KeyName}\"", sectionName, keyName); - return _configuration.GetValue>(sectionName, keyName); + return _configuration.GetValueOrDefault(sectionName, keyName, new List()); } /// @@ -51,7 +51,7 @@ public List GetListValue(string sectionName, string keyName) public Dictionary GetDictValue(string sectionName, string keyName) { _logger.Debug("Try getting config as dict value \"{SectionName}:{KeyName}\"", sectionName, keyName); - return _configuration.GetValue>(sectionName, keyName); + return _configuration.GetValueOrDefault(sectionName, keyName, new Dictionary()); } /// @@ -95,7 +95,7 @@ public void SetValue(string sectionName, string keyName, int value) /// public void SetValue(string sectionName, string keyName, IEnumerable value) { - _configuration.SetValue(sectionName, keyName, value.ToList()); + _configuration.SetValue(sectionName, keyName, value.ToArray()); } /// From e7c28d7f80a90f7fc19429cb1a97caf6a4217dfe Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 16:10:12 +0300 Subject: [PATCH 08/37] add override name to create config --- .../IniConfiguration.cs | 7 +++--- .../JsonConfiguration.cs | 7 +++--- .../YamlConfiguration.cs | 7 +++--- .../pyRevitLabs.PyRevit/PyRevitConfigs.cs | 24 +++++++++++++------ 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs index c32979412..946ca2699 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs @@ -9,7 +9,8 @@ namespace pyRevitLabs.Configurations.Ini; public sealed class IniConfiguration : ConfigurationBase { - public static readonly Encoding DefaultIniFileEncoding = new UTF8Encoding(false); + public static readonly string DefaultFileExtension = ".ini"; + public static readonly Encoding DefaultFileEncoding = new UTF8Encoding(false); private readonly IniData _iniFile; private readonly FileIniDataParser _parser; @@ -25,7 +26,7 @@ private IniConfiguration(string configurationPath, bool readOnly) _parser = new FileIniDataParser(); _iniFile = !File.Exists(configurationPath) ? new IniData() - : _parser.ReadFile(_configurationPath, DefaultIniFileEncoding); + : _parser.ReadFile(_configurationPath, DefaultFileEncoding); } /// @@ -46,7 +47,7 @@ public static IConfiguration Create(string configurationPath, bool readOnly = de /// protected override void SaveConfigurationImpl() { - _parser.WriteFile(_configurationPath, _iniFile, DefaultIniFileEncoding); + _parser.WriteFile(_configurationPath, _iniFile, DefaultFileEncoding); } /// diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs index ef06b2ad5..742e57c3c 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs @@ -10,7 +10,8 @@ namespace pyRevitLabs.Configurations.Json; public sealed class JsonConfiguration : ConfigurationBase { - public static readonly Encoding DefaultJsonFileEncoding = Encoding.UTF8; + public static readonly string DefaultFileExtension = ".json"; + public static readonly Encoding DefaultFileEncoding = Encoding.UTF8; private readonly JObject _jsonObject; @@ -24,7 +25,7 @@ private JsonConfiguration(string configurationPath, bool readOnly) { _jsonObject = !File.Exists(configurationPath) ? new JObject() - : JObject.Parse(File.ReadAllText(_configurationPath, DefaultJsonFileEncoding)); + : JObject.Parse(File.ReadAllText(_configurationPath, DefaultFileEncoding)); } /// @@ -46,7 +47,7 @@ protected override void SaveConfigurationImpl() { string jsonString = JsonConvert.SerializeObject(_jsonObject, new JsonSerializerSettings() {Formatting = Formatting.Indented}); - File.WriteAllText(_configurationPath, jsonString, DefaultJsonFileEncoding); + File.WriteAllText(_configurationPath, jsonString, DefaultFileEncoding); } protected override bool HasSectionImpl(string sectionName) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs index 5f2312563..fa530a38f 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs @@ -11,7 +11,8 @@ namespace pyRevitLabs.Configurations.Yaml; public sealed class YamlConfiguration : ConfigurationBase { - public static readonly Encoding DefaultYamlFileEncoding = Encoding.UTF8; + public static readonly string DefaultFileExtension = ".yml"; + public static readonly Encoding DefaultFileEncoding = Encoding.UTF8; private readonly YamlStream _yamlStream; private readonly YamlMappingNode _rootNode; @@ -27,7 +28,7 @@ private YamlConfiguration(string configurationPath, bool readOnly) _yamlStream = []; if (File.Exists(configurationPath)) { - _yamlStream.Load(new StringReader(File.ReadAllText(_configurationPath, DefaultYamlFileEncoding))); + _yamlStream.Load(new StringReader(File.ReadAllText(_configurationPath, DefaultFileEncoding))); } if (_yamlStream.Documents.Count == 0) @@ -57,7 +58,7 @@ protected override void SaveConfigurationImpl() { ISerializer serializer = CreateSerializer(); string yamlString = serializer.Serialize(_yamlStream); - File.WriteAllText(_configurationPath, yamlString, DefaultYamlFileEncoding); + File.WriteAllText(_configurationPath, yamlString, DefaultFileEncoding); } protected override bool HasSectionImpl(string sectionName) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs index 6a986e6c1..eceeb4334 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs @@ -6,6 +6,7 @@ using pyRevitLabs.Common; using pyRevitLabs.Configurations; using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Ini; using pyRevitLabs.Configurations.Ini.Extensions; using pyRevitLabs.NLog; @@ -26,7 +27,7 @@ public static class PyRevitConfigs /// Returns config file. /// /// Returns admin config if admin config exists and user config not found. - public static PyRevitConfig GetConfigFile() + public static PyRevitConfig GetConfigFile(string overrideName = default) { // make sure the file exists and if not create an empty one string userConfig = PyRevitConsts.ConfigFilePath; @@ -36,11 +37,11 @@ public static PyRevitConfig GetConfigFile() && File.Exists(adminConfig)) { _logger.Info("Creating admin config {@ConfigPath}", adminConfig); - return CreateConfiguration(adminConfig); + return CreateConfiguration(adminConfig, true, overrideName); } _logger.Info("Creating user config {@ConfigPath}", userConfig); - return CreateConfiguration(userConfig); + return CreateConfiguration(userConfig, false, overrideName); } /// @@ -125,12 +126,21 @@ public static void SetupConfig(string templateConfigFilePath = null) } } - private static PyRevitConfig CreateConfiguration(string configPath) + private static PyRevitConfig CreateConfiguration( + string configPath, + bool readOnly = false, + string overrideName = default) { - var configuration = new ConfigurationBuilder() - .AddIniConfiguration(configPath) - .Build(); + var builder = new ConfigurationBuilder() + .AddIniConfiguration(configPath, readOnly); + if (string.IsNullOrEmpty(overrideName)) + { + builder.AddIniConfiguration(Path.ChangeExtension(configPath, + $"{overrideName}.{IniConfiguration.DefaultFileExtension}")); + } + + var configuration = builder.Build(); return new PyRevitConfig(configuration); } From f4d0b36cfbbf344325a37bcceb5a604cc3c52394 Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 17:14:59 +0300 Subject: [PATCH 09/37] added configuration path and save --- .../IniConfiguration.cs | 8 ++++++- .../JsonConfiguration.cs | 9 +++++++- .../YamlConfiguration.cs | 9 +++++++- .../Abstractions/IConfiguration.cs | 5 +++++ .../ConfigurationBase.cs | 11 ++++++++++ .../ConfigurationService.cs | 22 ++++++++++++++++++- .../ConfigurationServiceFixture.cs | 12 ++++++++++ 7 files changed, 72 insertions(+), 4 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs index 946ca2699..f20acf642 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/IniConfiguration.cs @@ -47,7 +47,13 @@ public static IConfiguration Create(string configurationPath, bool readOnly = de /// protected override void SaveConfigurationImpl() { - _parser.WriteFile(_configurationPath, _iniFile, DefaultFileEncoding); + SaveConfigurationImpl(_configurationPath); + } + + /// + protected override void SaveConfigurationImpl(string configurationPath) + { + _parser.WriteFile(configurationPath, _iniFile, DefaultFileEncoding); } /// diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs index 742e57c3c..cb096717a 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs @@ -43,11 +43,18 @@ public static IConfiguration Create(string configurationPath, bool readOnly = de return new JsonConfiguration(configurationPath, readOnly); } + /// protected override void SaveConfigurationImpl() + { + SaveConfigurationImpl(_configurationPath); + } + + /// + protected override void SaveConfigurationImpl(string configurationPath) { string jsonString = JsonConvert.SerializeObject(_jsonObject, new JsonSerializerSettings() {Formatting = Formatting.Indented}); - File.WriteAllText(_configurationPath, jsonString, DefaultFileEncoding); + File.WriteAllText(configurationPath, jsonString, DefaultFileEncoding); } protected override bool HasSectionImpl(string sectionName) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs index fa530a38f..0065dcc99 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs @@ -54,11 +54,18 @@ public static IConfiguration Create(string configurationPath, bool readOnly = de return new YamlConfiguration(configurationPath, readOnly); } + /// protected override void SaveConfigurationImpl() + { + SaveConfigurationImpl(_configurationPath); + } + + /// + protected override void SaveConfigurationImpl(string configurationPath) { ISerializer serializer = CreateSerializer(); string yamlString = serializer.Serialize(_yamlStream); - File.WriteAllText(_configurationPath, yamlString, DefaultFileEncoding); + File.WriteAllText(configurationPath, yamlString, DefaultFileEncoding); } protected override bool HasSectionImpl(string sectionName) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs index b78c2275c..2c0aef4c2 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs @@ -2,6 +2,8 @@ namespace pyRevitLabs.Configurations.Abstractions; public interface IConfiguration { + string ConfigurationPath { get; } + bool HasSection(string sectionName); bool HasSectionKey(string sectionName, string keyName); @@ -10,4 +12,7 @@ public interface IConfiguration bool RemoveValue(string sectionName, string keyName); void SetValue(string sectionName, string keyName, T? value); + + void SaveConfiguration(); + void SaveConfiguration(string configurationPath); } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs index 10a7c0824..7cc34475e 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs @@ -8,6 +8,7 @@ public abstract class ConfigurationBase(string configurationPath, bool readOnly) protected readonly string _configurationPath = configurationPath; public bool ReadOnly { get; } = readOnly; + public string ConfigurationPath => _configurationPath; public void SaveConfiguration() { @@ -18,6 +19,14 @@ public void SaveConfiguration() SaveConfigurationImpl(); } + + public void SaveConfiguration(string configurationPath) + { + if (configurationPath == null) + throw new ArgumentNullException(nameof(configurationPath)); + + SaveConfigurationImpl(configurationPath); + } /// public bool HasSection(string sectionName) @@ -111,6 +120,8 @@ public void SetValue(string sectionName, string keyName, T? value) } protected abstract void SaveConfigurationImpl(); + protected abstract void SaveConfigurationImpl(string configurationPath); + protected abstract bool HasSectionImpl(string sectionName); protected abstract bool HasSectionKeyImpl(string sectionName, string keyName); diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index b6873f92b..e3078d08c 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -6,13 +6,17 @@ namespace pyRevitLabs.Configurations; public sealed class ConfigurationService(List configurations) : IConfiguration { public List Configurations { get; } = configurations; - public IEnumerable ReverseConfigurations => ((IEnumerable)Configurations).Reverse(); + + public IEnumerable ReverseConfigurations => + ((IEnumerable) Configurations).Reverse(); public static IConfiguration Create(List configurations) { return new ConfigurationService(configurations); } + public string ConfigurationPath => Configurations[0].ConfigurationPath; + public bool HasSection(string sectionName) { foreach (IConfiguration configuration in Configurations) @@ -92,4 +96,20 @@ public void SetValue(string sectionName, string keyName, T? value) Configurations[0].SetValue(sectionName, keyName, value); } + + public void SaveConfiguration() + { + foreach (IConfiguration configuration in Configurations) + { + configuration.SaveConfiguration(); + } + } + + public void SaveConfiguration(string configurationPath) + { + foreach (IConfiguration configuration in Configurations) + { + configuration.SaveConfiguration(configurationPath); + } + } } \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs index 3bf224a9b..a76509ee0 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs @@ -16,6 +16,8 @@ public void Dispose() { } private class TestRunConfiguration : IConfiguration { + public string ConfigurationPath { get; } + public bool HasSection(string sectionName) { throw new NotImplementedException(); @@ -45,6 +47,16 @@ public void SetValue(string sectionName, string keyName, T? value) { throw new NotImplementedException(); } + + public void SaveConfiguration() + { + throw new NotImplementedException(); + } + + public void SaveConfiguration(string configurationPath) + { + throw new NotImplementedException(); + } } } } \ No newline at end of file From b079c2f5dc94d1dabc3d5e4fc18c1f6ef63441fd Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 17:27:42 +0300 Subject: [PATCH 10/37] update service interface --- .../Extensions/IniConfigurationExtensions.cs | 8 +- .../Extensions/JsonConfigurationExtensions.cs | 8 +- .../Extensions/YamlConfigurationExtensions.cs | 8 +- .../ConfigurationBuilder.cs | 17 +-- .../ConfigurationService.cs | 107 +----------------- .../IniConfigurationUnitTests.cs | 14 +-- .../JsonConfigurationUnitTests.cs | 14 +-- .../ConfigurationServiceFixture.cs | 6 +- .../ConfigurationServiceUnitTests.cs | 6 +- .../YamlConfigurationUnitTests.cs | 14 +-- 10 files changed, 45 insertions(+), 157 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/Extensions/IniConfigurationExtensions.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/Extensions/IniConfigurationExtensions.cs index cae28966c..064073c22 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/Extensions/IniConfigurationExtensions.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Ini/Extensions/IniConfigurationExtensions.cs @@ -2,14 +2,18 @@ namespace pyRevitLabs.Configurations.Ini.Extensions; public static class IniConfigurationExtensions { - public static ConfigurationBuilder AddIniConfiguration(this ConfigurationBuilder builder, string configurationPath, bool readOnly = default) + public static ConfigurationBuilder AddIniConfiguration( + this ConfigurationBuilder builder, string configurationPath, string conigurationName, bool readOnly = default) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (string.IsNullOrWhiteSpace(configurationPath)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(configurationPath)); + + if (string.IsNullOrWhiteSpace(conigurationName)) + throw new ArgumentException("Value cannot be null or empty.", nameof(conigurationName)); - return builder.AddConfigurationSource(IniConfiguration.Create(configurationPath, readOnly)); + return builder.AddConfigurationSource(conigurationName, IniConfiguration.Create(configurationPath, readOnly)); } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/Extensions/JsonConfigurationExtensions.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/Extensions/JsonConfigurationExtensions.cs index c95207396..d189bc29b 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/Extensions/JsonConfigurationExtensions.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/Extensions/JsonConfigurationExtensions.cs @@ -2,14 +2,18 @@ namespace pyRevitLabs.Configurations.Json.Extensions; public static class JsonConfigurationExtensions { - public static ConfigurationBuilder AddJsonConfiguration(this ConfigurationBuilder builder, string configurationPath, bool readOnly = default) + public static ConfigurationBuilder AddJsonConfiguration( + this ConfigurationBuilder builder, string configurationPath, string conigurationName, bool readOnly = default) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (string.IsNullOrWhiteSpace(configurationPath)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(configurationPath)); + + if (string.IsNullOrWhiteSpace(conigurationName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(conigurationName)); - return builder.AddConfigurationSource(JsonConfiguration.Create(configurationPath, readOnly)); + return builder.AddConfigurationSource(conigurationName, JsonConfiguration.Create(configurationPath, readOnly)); } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/Extensions/YamlConfigurationExtensions.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/Extensions/YamlConfigurationExtensions.cs index ae696c77b..53a14221f 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/Extensions/YamlConfigurationExtensions.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/Extensions/YamlConfigurationExtensions.cs @@ -2,14 +2,18 @@ namespace pyRevitLabs.Configurations.Yaml.Extensions; public static class YamlConfigurationExtensions { - public static ConfigurationBuilder AddYamlConfiguration(this ConfigurationBuilder builder, string configurationPath, bool readOnly = default) + public static ConfigurationBuilder AddYamlConfiguration( + this ConfigurationBuilder builder, string configurationPath, string conigurationName, bool readOnly = default) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (string.IsNullOrWhiteSpace(configurationPath)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(configurationPath)); + + if (string.IsNullOrWhiteSpace(conigurationName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(conigurationName)); - return builder.AddConfigurationSource(YamlConfiguration.Create(configurationPath, readOnly)); + return builder.AddConfigurationSource(conigurationName, YamlConfiguration.Create(configurationPath, readOnly)); } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs index a0ca7120e..724d17123 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs @@ -4,18 +4,21 @@ namespace pyRevitLabs.Configurations; public sealed class ConfigurationBuilder { - private readonly List _configurations = []; - - public ConfigurationBuilder AddConfigurationSource(IConfiguration configuration) + private readonly Dictionary _configurations = []; + + public ConfigurationBuilder AddConfigurationSource(string configurationName, IConfiguration configuration) { - if (configuration == null) + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - - _configurations.Add(configuration); + + if (string.IsNullOrWhiteSpace(configurationName)) + throw new ArgumentException("Value cannot be null or empty.", nameof(configurationName)); + + _configurations.Add(configurationName, configuration); return this; } - public IConfiguration Build() + public ConfigurationService Build() { return ConfigurationService.Create(_configurations); } diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index e3078d08c..501a9c054 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -3,113 +3,12 @@ namespace pyRevitLabs.Configurations; -public sealed class ConfigurationService(List configurations) : IConfiguration +public sealed class ConfigurationService(IDictionary configurations) { - public List Configurations { get; } = configurations; + public IDictionary Configurations { get; } = configurations; - public IEnumerable ReverseConfigurations => - ((IEnumerable) Configurations).Reverse(); - - public static IConfiguration Create(List configurations) + public static ConfigurationService Create(IDictionary configurations) { return new ConfigurationService(configurations); } - - public string ConfigurationPath => Configurations[0].ConfigurationPath; - - public bool HasSection(string sectionName) - { - foreach (IConfiguration configuration in Configurations) - { - if (configuration.HasSection(sectionName)) - { - return true; - } - } - - return false; - } - - public bool HasSectionKey(string sectionName, string keyName) - { - foreach (IConfiguration configuration in ReverseConfigurations) - { - if (configuration.HasSectionKey(sectionName, keyName)) - { - return true; - } - } - - return false; - } - - public T GetValue(string sectionName, string keyName) - { - foreach (IConfiguration configuration in ReverseConfigurations) - { - if (configuration.HasSectionKey(sectionName, keyName)) - { - return configuration.GetValue(sectionName, keyName); - } - } - - throw new ConfigurationException("Section not found"); - } - - public T? GetValueOrDefault(string sectionName, string keyName, T? defaultValue = default) - { - foreach (IConfiguration configuration in ReverseConfigurations) - { - if (configuration.HasSectionKey(sectionName, keyName)) - { - return configuration.GetValueOrDefault(sectionName, keyName, defaultValue); - } - } - - return defaultValue; - } - - public bool RemoveValue(string sectionName, string keyName) - { - foreach (IConfiguration configuration in ReverseConfigurations) - { - if (configuration.HasSectionKey(sectionName, keyName)) - { - return configuration.RemoveValue(sectionName, keyName); - } - } - - return false; - } - - public void SetValue(string sectionName, string keyName, T? value) - { - // default save in main config - foreach (IConfiguration configuration in Configurations) - { - if (configuration.HasSectionKey(sectionName, keyName)) - { - configuration.SetValue(sectionName, keyName, value); - return; - } - } - - Configurations[0].SetValue(sectionName, keyName, value); - } - - public void SaveConfiguration() - { - foreach (IConfiguration configuration in Configurations) - { - configuration.SaveConfiguration(); - } - } - - public void SaveConfiguration(string configurationPath) - { - foreach (IConfiguration configuration in Configurations) - { - configuration.SaveConfiguration(configurationPath); - } - } } \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs index 47553fa7e..42390325b 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs @@ -26,23 +26,13 @@ public void CreateIniConfiguration_ShouldThrowsException() Assert.Throws(() => IniConfiguration.Create(default!)); } - [Fact] - public void CreateIniConfigurationByBuilder_ShouldCreate() - { - IConfiguration configuration = new ConfigurationBuilder() - .AddIniConfiguration(_configPath) - .Build(); - - Assert.NotNull(configuration); - } - [Fact] public void CreateIniConfigurationByBuilder_ShouldThrowsException() { Assert.Throws(() => { new ConfigurationBuilder() - .AddIniConfiguration(default!) + .AddIniConfiguration(default!, default!) .Build(); }); } @@ -53,7 +43,7 @@ public void CreateIniConfigurationByNullBuilder_ShouldThrowsException() Assert.Throws(() => { IniConfigurationExtensions - .AddIniConfiguration(default!, default!) + .AddIniConfiguration(default!, default!, default!) .Build(); }); } diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs index 85db53412..abc16097d 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs @@ -26,23 +26,13 @@ public void CreateIniConfiguration_ShouldThrowsException() Assert.Throws(() => JsonConfiguration.Create(default!)); } - [Fact] - public void CreateIniConfigurationByBuilder_ShouldCreate() - { - IConfiguration configuration = new ConfigurationBuilder() - .AddJsonConfiguration(_configPath) - .Build(); - - Assert.NotNull(configuration); - } - [Fact] public void CreateIniConfigurationByBuilder_ShouldThrowsException() { Assert.Throws(() => { new ConfigurationBuilder() - .AddJsonConfiguration(default!) + .AddJsonConfiguration(default!, default!) .Build(); }); } @@ -53,7 +43,7 @@ public void CreateIniConfigurationByNullBuilder_ShouldThrowsException() Assert.Throws(() => { JsonConfigurationExtensions - .AddJsonConfiguration(default!, default!) + .AddJsonConfiguration(default!, default!, default!) .Build(); }); } diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs index a76509ee0..15e830d58 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs @@ -7,10 +7,12 @@ public sealed class ConfigurationServiceFixture : IDisposable { public ConfigurationServiceFixture() { - Configuration = new ConfigurationService(new List() {new TestRunConfiguration()}); + Configuration = new ConfigurationService( + new Dictionary() + {{"default", new TestRunConfiguration()}}); } - public IConfiguration Configuration { get; } + public ConfigurationService Configuration { get; } public void Dispose() { } diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceUnitTests.cs index e6369cff1..a40e63a43 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceUnitTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceUnitTests.cs @@ -2,11 +2,13 @@ namespace pyRevitLabs.Configurations.Tests { - public sealed class ConfigurationServiceUnitTests : ConfigurationTests, IClassFixture + public sealed class ConfigurationServiceUnitTests : IClassFixture { + private readonly ConfigurationServiceFixture _configurationServiceFixture; + public ConfigurationServiceUnitTests(ConfigurationServiceFixture configurationServiceFixture) - : base(configurationServiceFixture.Configuration) { + _configurationServiceFixture = configurationServiceFixture; } } } \ No newline at end of file diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs index cfa23a9dc..6b473cbd7 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs @@ -26,23 +26,13 @@ public void CreateIniConfiguration_ShouldThrowsException() Assert.Throws(() => YamlConfiguration.Create(default!)); } - [Fact] - public void CreateIniConfigurationByBuilder_ShouldCreate() - { - IConfiguration configuration = new ConfigurationBuilder() - .AddYamlConfiguration(_configPath) - .Build(); - - Assert.NotNull(configuration); - } - [Fact] public void CreateIniConfigurationByBuilder_ShouldThrowsException() { Assert.Throws(() => { new ConfigurationBuilder() - .AddYamlConfiguration(default!) + .AddYamlConfiguration(default!, default!) .Build(); }); } @@ -53,7 +43,7 @@ public void CreateIniConfigurationByNullBuilder_ShouldThrowsException() Assert.Throws(() => { YamlConfigurationExtensions - .AddYamlConfiguration(default!, default!) + .AddYamlConfiguration(default!, default!, default!) .Build(); }); } From 1b528542c339e70a5e2f59ec0d1de1898fd55593 Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 18 Dec 2024 17:39:52 +0300 Subject: [PATCH 11/37] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Abstractions/IConfigurationService.cs | 6 ++++++ .../pyRevitLabs.Configurations/ConfigurationBuilder.cs | 2 +- .../pyRevitLabs.Configurations/ConfigurationService.cs | 9 +++++---- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs new file mode 100644 index 000000000..5c2c3448b --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs @@ -0,0 +1,6 @@ +namespace pyRevitLabs.Configurations.Abstractions; + +public interface IConfigurationService +{ + +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs index 724d17123..45b3713f2 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs @@ -18,7 +18,7 @@ public ConfigurationBuilder AddConfigurationSource(string configurationName, ICo return this; } - public ConfigurationService Build() + public IConfigurationService Build() { return ConfigurationService.Create(_configurations); } diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index 501a9c054..64f62f7d9 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -3,11 +3,12 @@ namespace pyRevitLabs.Configurations; -public sealed class ConfigurationService(IDictionary configurations) +public sealed class ConfigurationService(IDictionary configurations) : IConfigurationService { - public IDictionary Configurations { get; } = configurations; - - public static ConfigurationService Create(IDictionary configurations) + public IEnumerable ConfigurationNames => Configurations.Keys; + public IDictionary Configurations => configurations; + + public static IConfigurationService Create(IDictionary configurations) { return new ConfigurationService(configurations); } From 8618fadd8913c67f1f950575b249a9cecc5ab3b2 Mon Sep 17 00:00:00 2001 From: dosymep Date: Fri, 20 Dec 2024 14:35:58 +0300 Subject: [PATCH 12/37] fix spelling --- dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs | 9 +++++---- .../IniConfigurationUnitTests.cs | 6 +++--- .../JsonConfigurationUnitTests.cs | 6 +++--- .../ConfigurationTests.cs | 8 ++++---- .../YamlConfigurationUnitTests.cs | 8 ++++---- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs index eceeb4334..d4512313d 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs @@ -90,7 +90,7 @@ public static void SeedConfig(bool lockSeedConfig = false) _logger.Error(ex, $"You cannot assign ownership to user \"{currentUser.Name}\"." + "Either you don't have TakeOwnership permissions, " - + $"or it is not your user account."); + + "or it is not your user account."); } } } @@ -132,12 +132,13 @@ private static PyRevitConfig CreateConfiguration( string overrideName = default) { var builder = new ConfigurationBuilder() - .AddIniConfiguration(configPath, readOnly); + .AddIniConfiguration(configPath, "default", readOnly); if (string.IsNullOrEmpty(overrideName)) { - builder.AddIniConfiguration(Path.ChangeExtension(configPath, - $"{overrideName}.{IniConfiguration.DefaultFileExtension}")); + builder.AddIniConfiguration( + Path.ChangeExtension(configPath, + $"{overrideName}.{IniConfiguration.DefaultFileExtension}"), overrideName!); } var configuration = builder.Build(); diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs index 42390325b..e9811b4ac 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs @@ -21,13 +21,13 @@ public void CreateIniConfiguration_ShouldCreate() } [Fact] - public void CreateIniConfiguration_ShouldThrowsException() + public void CreateIniConfiguration_ShouldThrowException() { Assert.Throws(() => IniConfiguration.Create(default!)); } [Fact] - public void CreateIniConfigurationByBuilder_ShouldThrowsException() + public void CreateIniConfigurationByBuilder_ShouldThrowException() { Assert.Throws(() => { @@ -38,7 +38,7 @@ public void CreateIniConfigurationByBuilder_ShouldThrowsException() } [Fact] - public void CreateIniConfigurationByNullBuilder_ShouldThrowsException() + public void CreateIniConfigurationByNullBuilder_ShouldThrowException() { Assert.Throws(() => { diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs index abc16097d..fa030ca17 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs @@ -21,13 +21,13 @@ public void CreateIniConfiguration_ShouldCreate() } [Fact] - public void CreateIniConfiguration_ShouldThrowsException() + public void CreateJsonConfiguration_ShouldThrowException() { Assert.Throws(() => JsonConfiguration.Create(default!)); } [Fact] - public void CreateIniConfigurationByBuilder_ShouldThrowsException() + public void CreateJsonConfigurationByBuilder_ShouldThrowException() { Assert.Throws(() => { @@ -38,7 +38,7 @@ public void CreateIniConfigurationByBuilder_ShouldThrowsException() } [Fact] - public void CreateIniConfigurationByNullBuilder_ShouldThrowsException() + public void CreateJsonConfigurationByNullBuilder_ShouldThrowException() { Assert.Throws(() => { diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationTests.cs index d5755fa4b..17643a048 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationTests.cs @@ -44,7 +44,7 @@ private void StartUp() } [Fact] - public void NewCreateValue_ShouldReturnsSameValue() + public void NewCreateValue_ShouldReturnSameValue() { _configuration.SetValue("new", "create", "value"); @@ -54,7 +54,7 @@ public void NewCreateValue_ShouldReturnsSameValue() } [Fact] - public void GetValueOrDefault_ShouldReturnsSameValue() + public void GetValueOrDefault_ShouldReturnSameValue() { _configuration.SetValue("create", "default", "value"); @@ -64,7 +64,7 @@ public void GetValueOrDefault_ShouldReturnsSameValue() } [Fact] - public void GetValueOrDefault_ShouldReturnsDefaultValue() + public void GetValueOrDefault_ShouldReturnDefaultValue() { string? value = _configuration.GetValueOrDefault("not_exits", "key", "defaultValue"); @@ -72,7 +72,7 @@ public void GetValueOrDefault_ShouldReturnsDefaultValue() } [Fact] - public void RemoveExitsValue_ShouldReturnsTrue() + public void RemoveExitsValue_ShouldReturnTrue() { _configuration.SetValue("remove", "default", "value"); diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs index 6b473cbd7..a832294bb 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs @@ -15,19 +15,19 @@ public YamlConfigurationUnitTests(YamlCreateFixture createFixture) } [Fact] - public void CreateIniConfiguration_ShouldCreate() + public void CreateYamlConfiguration_ShouldCreate() { Assert.NotNull(YamlConfiguration.Create(_configPath)); } [Fact] - public void CreateIniConfiguration_ShouldThrowsException() + public void CreateYamlConfiguration_ShouldThrowException() { Assert.Throws(() => YamlConfiguration.Create(default!)); } [Fact] - public void CreateIniConfigurationByBuilder_ShouldThrowsException() + public void CreateYamlConfigurationByBuilder_ShouldThrowException() { Assert.Throws(() => { @@ -38,7 +38,7 @@ public void CreateIniConfigurationByBuilder_ShouldThrowsException() } [Fact] - public void CreateIniConfigurationByNullBuilder_ShouldThrowsException() + public void CreateYamlConfigurationByNullBuilder_ShouldThrowException() { Assert.Throws(() => { From 8e4eddfa0a4d4b028e0fe33adbd8dd914cb63538 Mon Sep 17 00:00:00 2001 From: dosymep Date: Fri, 20 Dec 2024 15:22:37 +0300 Subject: [PATCH 13/37] add sections classes --- .../pyRevitLabs.Configurations/Constants.cs | 52 +++++++++++++++++++ .../pyRevitLabs.Configurations/CoreSection.cs | 36 +++++++++++++ .../RoutesSection.cs | 10 ++++ .../TelemetrySection.cs | 14 +++++ 4 files changed, 112 insertions(+) create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/Constants.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Constants.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Constants.cs new file mode 100644 index 000000000..801de2b5c --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Constants.cs @@ -0,0 +1,52 @@ +namespace pyRevitLabs.Configurations; + +internal static class Constants +{ + // core + public const string CoreSection = "core"; + public const string BinaryCacheKey = "bincache"; + public const string CheckUpdatesKey = "checkupdates"; + public const string AutoUpdateKey = "autoupdate"; + public const string RocketModeKey = "rocketmode"; + public const string VerboseKey = "verbose"; + public const string DebugKey = "debug"; + public const string FileLoggingKey = "filelogging"; + public const string StartupLogTimeoutKey = "startuplogtimeout"; + public const string RequiredHostBuildKey = "requiredhostbuild"; + public const string MinDriveSpaceKey = "minhostdrivefreespace"; + public const string LoadBetaKey = "loadbeta"; + public const string CPythonEngineKey = "cpyengine"; + public const string LocaleKey = "user_locale"; + public const string OutputStyleSheet = "outputstylesheet"; + public const string UserExtensionsKey = "userextensions"; + public const string UserCanUpdateKey = "usercanupdate"; + public const string UserCanExtendKey = "usercanextend"; + public const string UserCanConfigKey = "usercanconfig"; + public const string ColorizeDocsKey = "colorize_docs"; + public const string AppendTooltipExKey = "tooltip_debug_info"; + + // routes + public const string RoutesSection = "routes"; + public const string RoutesServerKey = "enabled"; + public const string RoutesHostKey = "host"; + public const string RoutesPortKey = "port"; + public const string LoadCoreAPIKey = "core_api"; + + // telemetry + public const string TelemetrySection = "telemetry"; + public const string TelemetryUTCTimestampsKey = "utc_timestamps"; + public const string TelemetryStatusKey = "active"; + public const string TelemetryFileDirKey = "telemetry_file_dir"; + public const string TelemetryServerUrlKey = "telemetry_server_url"; + public const string TelemetryIncludeHooksKey = "include_hooks"; + public const string AppTelemetryStatusKey = "active_app"; + public const string AppTelemetryServerUrlKey = "apptelemetry_server_url"; + public const string AppTelemetryEventFlagsKey = "apptelemetry_event_flags"; + + // pyrevit.exe specific + public const string EnvSectionName = "environment"; + public const string EnvInstalledClonesKey = "clones"; + public const string EnvExtensionLookupSourcesKey = "sources"; + public const string EnvTemplateSourcesKey = "templates"; + public const string EnvExtensionDBFileName = "PyRevitExtensionsDB.json"; +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs new file mode 100644 index 000000000..250f77fc8 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs @@ -0,0 +1,36 @@ +namespace pyRevitLabs.Configurations; + +public enum LogLevels +{ + Quiet, + Verbose, + Debug +} + +internal sealed record CoreSection +{ + public bool BinCache { get; set; } + + public bool LoadBeta { get; set; } + public bool AutoUpdate { get; set; } + public bool CheckUpdates { get; set; } + + public bool UserCanUpdate { get; set; } = true; + public bool UserCanExtend { get; set; } = true; + public bool UserCanConfig { get; set; } = true; + + public bool RocketMode { get; set; } = true; + public string UserLocale { get; set; } = "en_us"; + + public LogLevels LogLevel { get; set; } + public bool FileLogging { get; set; } + public int StartupLogTimeout { get; set; } = 10; + + public int CpythonEngineVersion { get; set; } + public bool RequiredHostBuild { get; set; } + public long MinHostDriveFreeSpace { get; set; } + + public bool ColorizeDocs { get; set; } + public bool TooltipDebugInfo { get; set; } + public string? OutputStylesheet { get; set; } +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs new file mode 100644 index 000000000..7d7f7ff0f --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs @@ -0,0 +1,10 @@ +namespace pyRevitLabs.Configurations; + +internal sealed record RoutesSection +{ + public bool Status { get; set; } + public bool LoadCoreApi { get; set; } + + public string? Host { get; set; } + public int Port { get; set; } = 48884; +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs new file mode 100644 index 000000000..596310ecd --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs @@ -0,0 +1,14 @@ +namespace pyRevitLabs.Configurations; + +internal record TelemetrySection +{ + public bool UtcTimeStamps { get; set; } + public bool Status { get; set; } + public string? FileDir { get; set; } + public string? ServerUrl { get; set; } + public bool IncludeHooks { get; set; } + + public bool AppTelemetryStatus { get; set; } + public string? AppTelemetryServerUrl { get; set; } + public int AppTelemetryEventFlags { get; set; } +} \ No newline at end of file From a22a053e71dd2084f874995c6e762483e8ca831b Mon Sep 17 00:00:00 2001 From: dosymep Date: Mon, 23 Dec 2024 11:55:04 +0300 Subject: [PATCH 14/37] update --- .../pyRevitLabs.Configurations/RoutesSection.cs | 2 +- .../pyRevitLabs.Configurations/TelemetrySection.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs index 7d7f7ff0f..1a22871a7 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs @@ -3,8 +3,8 @@ internal sealed record RoutesSection { public bool Status { get; set; } - public bool LoadCoreApi { get; set; } public string? Host { get; set; } public int Port { get; set; } = 48884; + public bool LoadCoreApi { get; set; } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs index 596310ecd..4c4eacd6f 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs @@ -2,11 +2,12 @@ internal record TelemetrySection { - public bool UtcTimeStamps { get; set; } - public bool Status { get; set; } - public string? FileDir { get; set; } - public string? ServerUrl { get; set; } - public bool IncludeHooks { get; set; } + public bool TelemetryStatus { get; set; } + + public bool TelemetryUseUtcTimeStamps { get; set; } + public string? TelemetryFileDir { get; set; } + public string? TelemetryServerUrl { get; set; } + public bool TelemetryIncludeHooks { get; set; } public bool AppTelemetryStatus { get; set; } public string? AppTelemetryServerUrl { get; set; } From caea3f3be1197355ca0533e6a3e7e87f7bc83ab5 Mon Sep 17 00:00:00 2001 From: dosymep Date: Mon, 23 Dec 2024 12:13:21 +0300 Subject: [PATCH 15/37] add configuration name --- .../pyRevitLabs.Configurations/ConfigurationBuilder.cs | 6 ++++-- .../pyRevitLabs.Configurations/ConfigurationName.cs | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationName.cs diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs index 45b3713f2..c0d65896c 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs @@ -4,7 +4,7 @@ namespace pyRevitLabs.Configurations; public sealed class ConfigurationBuilder { - private readonly Dictionary _configurations = []; + private readonly Dictionary _configurations = []; public ConfigurationBuilder AddConfigurationSource(string configurationName, IConfiguration configuration) { @@ -14,7 +14,9 @@ public ConfigurationBuilder AddConfigurationSource(string configurationName, ICo if (string.IsNullOrWhiteSpace(configurationName)) throw new ArgumentException("Value cannot be null or empty.", nameof(configurationName)); - _configurations.Add(configurationName, configuration); + _configurations.Add( + new ConfigurationName {Index = _configurations.Count, Name = configurationName}, configuration); + return this; } diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationName.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationName.cs new file mode 100644 index 000000000..37cfc65a3 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationName.cs @@ -0,0 +1,8 @@ +namespace pyRevitLabs.Configurations; + +public sealed record ConfigurationName +{ + + public required int Index { get; init; } + public required string Name { get; init; } +} \ No newline at end of file From da4575d3b40f2a41f35211524f2efbe9b170a344 Mon Sep 17 00:00:00 2001 From: dosymep Date: Mon, 23 Dec 2024 12:13:41 +0300 Subject: [PATCH 16/37] add support requred and init features for net48 --- .../System.Runtime.CompilerServices.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/System.Runtime.CompilerServices.cs diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/System.Runtime.CompilerServices.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/System.Runtime.CompilerServices.cs new file mode 100644 index 000000000..e79280797 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/System.Runtime.CompilerServices.cs @@ -0,0 +1,40 @@ +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ +#if !NET5_0_OR_GREATER + + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } + +#endif // !NET5_0_OR_GREATER + +#if !NET7_0_OR_GREATER + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct + | AttributeTargets.Field | AttributeTargets.Property, + AllowMultiple = false, Inherited = false)] + internal sealed class RequiredMemberAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class CompilerFeatureRequiredAttribute : Attribute + { + public CompilerFeatureRequiredAttribute(string featureName) + { + FeatureName = featureName; + } + + public string FeatureName { get; } + public bool IsOptional { get; init; } + + public const string RefStructs = nameof(RefStructs); + public const string RequiredMembers = nameof(RequiredMembers); + } + +#endif // !NET7_0_OR_GREATER +} \ No newline at end of file From 1661d2e9d37baae54c781ea52c1b57a170b1d231 Mon Sep 17 00:00:00 2001 From: dosymep Date: Mon, 23 Dec 2024 13:24:21 +0300 Subject: [PATCH 17/37] add parse config by reflection --- bin/netcore/pyRevitLabs.Emojis.dll | Bin 3513344 -> 3516416 bytes bin/netfx/pyRevitLabs.Emojis.dll | Bin 3512832 -> 3515904 bytes .../IniConfiguration.cs | 14 +-- .../JsonConfiguration.cs | 22 ++-- .../YamlConfiguration.cs | 17 +-- .../Abstractions/IConfiguration.cs | 3 + .../Attributes/KeyNameAttribute.cs | 8 ++ .../Attributes/SectionNameAttribute.cs | 7 ++ .../ConfigurationBase.cs | 18 +++- .../ConfigurationBuilder.cs | 12 ++- .../ConfigurationName.cs | 7 +- .../ConfigurationService.cs | 100 ++++++++++++++++-- .../pyRevitLabs.Configurations/CoreSection.cs | 2 +- .../RoutesSection.cs | 2 +- .../System.Runtime.CompilerServices.cs | 40 ------- .../TelemetrySection.cs | 2 +- .../pyRevitLabs.Configurations.csproj | 6 ++ .../ConfigurationServiceFixture.cs | 11 ++ ...RevitLabs.Configurations.Yaml.Tests.csproj | 2 +- 19 files changed, 188 insertions(+), 85 deletions(-) create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/Attributes/KeyNameAttribute.cs create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/Attributes/SectionNameAttribute.cs delete mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/System.Runtime.CompilerServices.cs diff --git a/bin/netcore/pyRevitLabs.Emojis.dll b/bin/netcore/pyRevitLabs.Emojis.dll index ca3af7d6ddead3f961425c2f15ce4f464250acb9..d1b6fd4a0e02128ffdbad6c99815c26451f9df8b 100644 GIT binary patch delta 48190 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 U=Y}zwdc{ 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 delta 44907 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 - protected override T GetValueImpl(string sectionName, string keyName) - { - return JsonConvert.DeserializeObject(_iniFile[sectionName][keyName]) - ?? throw new ConfigurationException("Cannot deserialize value using the specified key."); - } - /// protected override void SetValueImpl(string sectionName, string keyName, T value) { @@ -97,4 +90,11 @@ protected override void SetValueImpl(string sectionName, string keyName, T va _iniFile[sectionName][keyName] = JsonConvert.SerializeObject(value); } + + /// + protected override object GetValueImpl(Type typeObject, string sectionName, string keyName) + { + return JsonConvert.DeserializeObject(_iniFile[sectionName][keyName], typeObject) + ?? throw new ConfigurationException("Cannot deserialize value using the specified key."); + } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs index cb096717a..fb8091340 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Json/JsonConfiguration.cs @@ -57,11 +57,13 @@ protected override void SaveConfigurationImpl(string configurationPath) File.WriteAllText(configurationPath, jsonString, DefaultFileEncoding); } + /// protected override bool HasSectionImpl(string sectionName) { return _jsonObject.ContainsKey(sectionName); } + /// protected override bool HasSectionKeyImpl(string sectionName, string keyName) { JObject? sectionObject = _jsonObject[sectionName] as JObject; @@ -74,15 +76,7 @@ protected override bool RemoveValueImpl(string sectionName, string keyName) return true; } - protected override T GetValueImpl(string sectionName, string keyName) - { - JToken? token = _jsonObject[sectionName]?[keyName]; - return token is null - ? throw new ConfigurationException($"Section {sectionName} or keyName {keyName} not found.") - : token.ToObject() - ?? throw new ConfigurationException($"Cannot deserialize value with {sectionName} and {keyName}."); - } - + /// protected override void SetValueImpl(string sectionName, string keyName, T value) { if (!HasSection(sectionName)) @@ -108,4 +102,14 @@ protected override void SetValueImpl(string sectionName, string keyName, T va _jsonObject[sectionName]![keyName] = JToken.FromObject(value!); } + + /// + protected override object GetValueImpl(Type typeObject, string sectionName, string keyName) + { + JToken? token = _jsonObject[sectionName]?[keyName]; + return token is null + ? throw new ConfigurationException($"Section {sectionName} or keyName {keyName} not found.") + : token.ToObject(typeObject) + ?? throw new ConfigurationException($"Cannot deserialize value with {sectionName} and {keyName}."); + } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs index 0065dcc99..7b468d415 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations.Yaml/YamlConfiguration.cs @@ -68,12 +68,14 @@ protected override void SaveConfigurationImpl(string configurationPath) File.WriteAllText(configurationPath, yamlString, DefaultFileEncoding); } + /// protected override bool HasSectionImpl(string sectionName) { return _rootNode.Children.ContainsKey(sectionName) && _rootNode.Children[sectionName].NodeType == YamlNodeType.Mapping; } + /// protected override bool HasSectionKeyImpl(string sectionName, string keyName) { if (!HasSection(sectionName)) @@ -91,18 +93,14 @@ protected override bool HasSectionKeyImpl(string sectionName, string keyName) return yamlNodeType is YamlNodeType.Scalar or YamlNodeType.Sequence or YamlNodeType.Mapping; } + /// protected override bool RemoveValueImpl(string sectionName, string keyName) { YamlMappingNode yamlNode = (YamlMappingNode)_rootNode[sectionName]; return yamlNode.Children.Remove(keyName); } - protected override T GetValueImpl(string sectionName, string keyName) - { - YamlNode yamlNode = _rootNode[sectionName][keyName]; - return CreateDeserializer().Deserialize(yamlNode.ToString()); - } - + /// protected override void SetValueImpl(string sectionName, string keyName, T value) { if (_yamlStream.Documents.Count == 0) @@ -148,4 +146,11 @@ private static IDeserializer CreateDeserializer() .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); } + + protected override object GetValueImpl(Type typeObject, string sectionName, string keyName) + { + YamlNode yamlNode = _rootNode[sectionName][keyName]; + return CreateDeserializer().Deserialize(yamlNode.ToString(), typeObject) + ?? throw new ConfigurationException($"Cannot deserialize value with {sectionName} and {keyName}."); + } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs index 2c0aef4c2..9eb76c0e5 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfiguration.cs @@ -9,6 +9,9 @@ public interface IConfiguration T GetValue(string sectionName, string keyName); T? GetValueOrDefault(string sectionName, string keyName, T? defaultValue = default); + + internal object GetValue(Type typeObject, string sectionName, string keyName); + internal object? GetValueOrDefault(Type typeObject, string sectionName, string keyName, object? defaultValue = default); bool RemoveValue(string sectionName, string keyName); void SetValue(string sectionName, string keyName, T? value); diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Attributes/KeyNameAttribute.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Attributes/KeyNameAttribute.cs new file mode 100644 index 000000000..7301808e8 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Attributes/KeyNameAttribute.cs @@ -0,0 +1,8 @@ +namespace pyRevitLabs.Configurations.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +internal sealed class KeyNameAttribute(string keyName) : Attribute +{ + + public string KeyName { get; } = keyName; +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Attributes/SectionNameAttribute.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Attributes/SectionNameAttribute.cs new file mode 100644 index 000000000..a660705b1 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Attributes/SectionNameAttribute.cs @@ -0,0 +1,7 @@ +namespace pyRevitLabs.Configurations.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +internal sealed class SectionNameAttribute(string sectionName) : Attribute +{ + public string SectionName { get; } = sectionName; +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs index 7cc34475e..abd8770ea 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs @@ -46,6 +46,11 @@ public bool HasSectionKey(string sectionName, string keyName) return HasSectionKeyImpl(sectionName, keyName); } + public object? GetValueOrDefault(Type typeObject, string sectionName, string keyName, object? defaultValue = default) + { + throw new NotImplementedException(); + } + /// public bool RemoveValue(string sectionName, string keyName) { @@ -82,7 +87,7 @@ public T GetValue(string sectionName, string keyName) if (!HasSectionKey(sectionName, keyName)) throw new ConfigurationSectionKeyNotFoundException(sectionName, keyName); - return GetValueImpl(sectionName, keyName); + return (T) GetValueImpl(typeof(T), sectionName, keyName); } /// @@ -100,7 +105,12 @@ public T GetValue(string sectionName, string keyName) if (!HasSectionKey(sectionName, keyName)) return defaultValue; - return GetValueImpl(sectionName, keyName); + return (T) GetValueImpl(typeof(T), sectionName, keyName); + } + + public object GetValue(Type typeObject, string sectionName, string keyName) + { + throw new NotImplementedException(); } /// @@ -126,7 +136,7 @@ public void SetValue(string sectionName, string keyName, T? value) protected abstract bool HasSectionKeyImpl(string sectionName, string keyName); protected abstract bool RemoveValueImpl(string sectionName, string keyName); - - protected abstract T GetValueImpl(string sectionName, string keyName); + protected abstract void SetValueImpl(string sectionName, string keyName, T value); + protected abstract object GetValueImpl(Type typeObject, string sectionName, string keyName); } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs index c0d65896c..c7edf5e62 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs @@ -1,10 +1,12 @@ +using System.Collections.Specialized; using pyRevitLabs.Configurations.Abstractions; namespace pyRevitLabs.Configurations; public sealed class ConfigurationBuilder { - private readonly Dictionary _configurations = []; + private readonly List _names = []; + private readonly Dictionary _configurations = []; public ConfigurationBuilder AddConfigurationSource(string configurationName, IConfiguration configuration) { @@ -14,14 +16,14 @@ public ConfigurationBuilder AddConfigurationSource(string configurationName, ICo if (string.IsNullOrWhiteSpace(configurationName)) throw new ArgumentException("Value cannot be null or empty.", nameof(configurationName)); - _configurations.Add( - new ConfigurationName {Index = _configurations.Count, Name = configurationName}, configuration); - + _names.Add(new ConfigurationName() {Index = _configurations.Count, Name = configurationName}); + _configurations.Add(configurationName, configuration); + return this; } public IConfigurationService Build() { - return ConfigurationService.Create(_configurations); + return ConfigurationService.Create(_names, _configurations); } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationName.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationName.cs index 37cfc65a3..44ab7d756 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationName.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationName.cs @@ -1,8 +1,7 @@ namespace pyRevitLabs.Configurations; -public sealed record ConfigurationName +internal sealed record ConfigurationName { - - public required int Index { get; init; } - public required string Name { get; init; } + public int Index { get; set; } + public string? Name { get; set; } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index 64f62f7d9..2f1d8e369 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -1,15 +1,103 @@ +using System.Collections; +using System.Reflection; using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Attributes; using pyRevitLabs.Configurations.Exceptions; namespace pyRevitLabs.Configurations; -public sealed class ConfigurationService(IDictionary configurations) : IConfigurationService +public sealed class ConfigurationService : IConfigurationService { - public IEnumerable ConfigurationNames => Configurations.Keys; - public IDictionary Configurations => configurations; - - public static IConfigurationService Create(IDictionary configurations) + private readonly List _names; + private readonly IDictionary _configurations; + + public static readonly string DefaultConfigurationName = "Default"; + + internal ConfigurationService( + List names, + IDictionary configurations) + { + _names = names; + _configurations = configurations; + } + + internal static IConfigurationService Create( + List names, + IDictionary configurations) + { + return new ConfigurationService(names, configurations); + } + + public IEnumerable ConfigurationNames => _configurations.Keys; + public IEnumerable Configurations => _configurations.Values; + + public CoreSection? Core { get; private set; } + public RoutesSection? Routes { get; private set; } + public TelemetrySection? Telemetry { get; private set; } + + public void LoadConfigurations() + { + Core = GetConfiguration(); + Routes = GetConfiguration(); + Telemetry = GetConfiguration(); + } + + internal T GetConfiguration() + { + return GetConfigurationImpl(DefaultConfigurationName); + } + + internal T GetConfiguration(string configurationName) + { + return GetConfigurationImpl(configurationName); + } + + internal T? GetConfigurationOrDefault(T? defaultValue = default) + { + return GetConfigurationOrDefault(DefaultConfigurationName, defaultValue); + } + + internal T? GetConfigurationOrDefault(string configurationName, T? defaultValue = default) + { + try + { + return GetConfigurationImpl(configurationName); + } + catch + { + return defaultValue; + } + } + + private T GetConfigurationImpl(string configurationName) + { + if (string.IsNullOrWhiteSpace(configurationName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(configurationName)); + + if (!_configurations.TryGetValue(configurationName, out var configuration)) + throw new ArgumentException($"Configuration with name {configurationName} not found"); + + Type configurationType = typeof(T); + + string sectionName = + GetCustomAttribute(configurationType)?.SectionName ?? configurationType.Name; + + var sectionConfiguration = Activator.CreateInstance(); + foreach (var propertyInfo in configurationType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + string keyName = GetCustomAttribute(propertyInfo)?.KeyName ?? propertyInfo.Name; + + object? keyValue = configuration.GetValueOrDefault( + propertyInfo.PropertyType, sectionName, keyName, propertyInfo.GetValue(sectionConfiguration)); + + propertyInfo.SetValue(sectionConfiguration, keyValue); + } + + return sectionConfiguration; + } + + private static T? GetCustomAttribute(MemberInfo memberInfo) where T : Attribute { - return new ConfigurationService(configurations); + return memberInfo.GetCustomAttributes(typeof(T), false).FirstOrDefault() as T; } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs index 250f77fc8..bac2d4eb4 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs @@ -7,7 +7,7 @@ public enum LogLevels Debug } -internal sealed record CoreSection +public sealed record CoreSection { public bool BinCache { get; set; } diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs index 1a22871a7..05603aca3 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs @@ -1,6 +1,6 @@ namespace pyRevitLabs.Configurations; -internal sealed record RoutesSection +public sealed record RoutesSection { public bool Status { get; set; } diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/System.Runtime.CompilerServices.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/System.Runtime.CompilerServices.cs deleted file mode 100644 index e79280797..000000000 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/System.Runtime.CompilerServices.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.ComponentModel; - -// ReSharper disable once CheckNamespace -namespace System.Runtime.CompilerServices -{ -#if !NET5_0_OR_GREATER - - [EditorBrowsable(EditorBrowsableState.Never)] - internal static class IsExternalInit - { - } - -#endif // !NET5_0_OR_GREATER - -#if !NET7_0_OR_GREATER - - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct - | AttributeTargets.Field | AttributeTargets.Property, - AllowMultiple = false, Inherited = false)] - internal sealed class RequiredMemberAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] - internal sealed class CompilerFeatureRequiredAttribute : Attribute - { - public CompilerFeatureRequiredAttribute(string featureName) - { - FeatureName = featureName; - } - - public string FeatureName { get; } - public bool IsOptional { get; init; } - - public const string RefStructs = nameof(RefStructs); - public const string RequiredMembers = nameof(RequiredMembers); - } - -#endif // !NET7_0_OR_GREATER -} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs index 4c4eacd6f..c4fe3a21a 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs @@ -1,6 +1,6 @@ namespace pyRevitLabs.Configurations; -internal record TelemetrySection +public record TelemetrySection { public bool TelemetryStatus { get; set; } diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/pyRevitLabs.Configurations.csproj b/dev/pyRevitLabs/pyRevitLabs.Configurations/pyRevitLabs.Configurations.csproj index 849661bbf..890effb41 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/pyRevitLabs.Configurations.csproj +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/pyRevitLabs.Configurations.csproj @@ -6,5 +6,11 @@ enable 12 + + + + <_Parameter1>pyRevitLabs.Configurations.Tests + + diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs index 15e830d58..5b46ba454 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs @@ -8,6 +8,7 @@ public sealed class ConfigurationServiceFixture : IDisposable public ConfigurationServiceFixture() { Configuration = new ConfigurationService( + new List(), new Dictionary() {{"default", new TestRunConfiguration()}}); } @@ -40,6 +41,16 @@ public T GetValue(string sectionName, string keyName) throw new NotImplementedException(); } + public object GetValue(Type typeObject, string sectionName, string keyName) + { + throw new NotImplementedException(); + } + + public object? GetValueOrDefault(Type typeObject, string sectionName, string keyName, object? defaultValue = default) + { + throw new NotImplementedException(); + } + public bool RemoveValue(string sectionName, string keyName) { throw new NotImplementedException(); diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj index 4698eb69e..a9c5955d3 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/pyRevitLabs.Configurations.Yaml.Tests.csproj @@ -21,8 +21,8 @@ + - From 29bc6ef83de4966a285779d3243966bcf71a02f4 Mon Sep 17 00:00:00 2001 From: dosymep Date: Mon, 23 Dec 2024 14:00:07 +0300 Subject: [PATCH 18/37] add attribute settings --- .../pyRevitLabs.Configurations/Constants.cs | 41 ------------------- .../pyRevitLabs.Configurations/CoreSection.cs | 34 ++++++++++++++- .../RoutesSection.cs | 11 ++++- .../TelemetrySection.cs | 18 +++++++- 4 files changed, 60 insertions(+), 44 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Constants.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Constants.cs index 801de2b5c..6a62a45fb 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/Constants.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Constants.cs @@ -2,47 +2,6 @@ internal static class Constants { - // core - public const string CoreSection = "core"; - public const string BinaryCacheKey = "bincache"; - public const string CheckUpdatesKey = "checkupdates"; - public const string AutoUpdateKey = "autoupdate"; - public const string RocketModeKey = "rocketmode"; - public const string VerboseKey = "verbose"; - public const string DebugKey = "debug"; - public const string FileLoggingKey = "filelogging"; - public const string StartupLogTimeoutKey = "startuplogtimeout"; - public const string RequiredHostBuildKey = "requiredhostbuild"; - public const string MinDriveSpaceKey = "minhostdrivefreespace"; - public const string LoadBetaKey = "loadbeta"; - public const string CPythonEngineKey = "cpyengine"; - public const string LocaleKey = "user_locale"; - public const string OutputStyleSheet = "outputstylesheet"; - public const string UserExtensionsKey = "userextensions"; - public const string UserCanUpdateKey = "usercanupdate"; - public const string UserCanExtendKey = "usercanextend"; - public const string UserCanConfigKey = "usercanconfig"; - public const string ColorizeDocsKey = "colorize_docs"; - public const string AppendTooltipExKey = "tooltip_debug_info"; - - // routes - public const string RoutesSection = "routes"; - public const string RoutesServerKey = "enabled"; - public const string RoutesHostKey = "host"; - public const string RoutesPortKey = "port"; - public const string LoadCoreAPIKey = "core_api"; - - // telemetry - public const string TelemetrySection = "telemetry"; - public const string TelemetryUTCTimestampsKey = "utc_timestamps"; - public const string TelemetryStatusKey = "active"; - public const string TelemetryFileDirKey = "telemetry_file_dir"; - public const string TelemetryServerUrlKey = "telemetry_server_url"; - public const string TelemetryIncludeHooksKey = "include_hooks"; - public const string AppTelemetryStatusKey = "active_app"; - public const string AppTelemetryServerUrlKey = "apptelemetry_server_url"; - public const string AppTelemetryEventFlagsKey = "apptelemetry_event_flags"; - // pyrevit.exe specific public const string EnvSectionName = "environment"; public const string EnvInstalledClonesKey = "clones"; diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs index bac2d4eb4..b46f95ada 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs @@ -1,4 +1,6 @@ -namespace pyRevitLabs.Configurations; +using pyRevitLabs.Configurations.Attributes; + +namespace pyRevitLabs.Configurations; public enum LogLevels { @@ -7,30 +9,60 @@ public enum LogLevels Debug } +[SectionName("core")] public sealed record CoreSection { + [KeyName("bincache")] public bool BinCache { get; set; } + [KeyName("loadbeta")] public bool LoadBeta { get; set; } + + [KeyName("autoupdate")] public bool AutoUpdate { get; set; } + + [KeyName("checkupdates")] public bool CheckUpdates { get; set; } + [KeyName("usercanupdate")] public bool UserCanUpdate { get; set; } = true; + + [KeyName("usercanextend")] public bool UserCanExtend { get; set; } = true; + + [KeyName("usercanconfig")] public bool UserCanConfig { get; set; } = true; + [KeyName("rocketmode")] public bool RocketMode { get; set; } = true; + + [KeyName("user_locale")] public string UserLocale { get; set; } = "en_us"; + [KeyName("log_level")] public LogLevels LogLevel { get; set; } + + [KeyName("filelogging")] public bool FileLogging { get; set; } + + [KeyName("startuplogtimeout")] public int StartupLogTimeout { get; set; } = 10; + [KeyName("cpyengine")] public int CpythonEngineVersion { get; set; } + + [KeyName("requiredhostbuild")] public bool RequiredHostBuild { get; set; } + + [KeyName("minhostdrivefreespace")] public long MinHostDriveFreeSpace { get; set; } + [KeyName("colorize_docs")] public bool ColorizeDocs { get; set; } + + [KeyName("tooltip_debug_info")] public bool TooltipDebugInfo { get; set; } + + [KeyName("outputstylesheet")] public string? OutputStylesheet { get; set; } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs index 05603aca3..e61614dd1 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs @@ -1,10 +1,19 @@ -namespace pyRevitLabs.Configurations; +using pyRevitLabs.Configurations.Attributes; +namespace pyRevitLabs.Configurations; + +[SectionName("routes")] public sealed record RoutesSection { + [KeyName("enabled")] public bool Status { get; set; } + [KeyName("host")] public string? Host { get; set; } + + [KeyName("port")] public int Port { get; set; } = 48884; + + [KeyName("core_api")] public bool LoadCoreApi { get; set; } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs index c4fe3a21a..03c5dd173 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs @@ -1,15 +1,31 @@ -namespace pyRevitLabs.Configurations; +using pyRevitLabs.Configurations.Attributes; +namespace pyRevitLabs.Configurations; + +[SectionName("telemetry")] public record TelemetrySection { + [KeyName("active")] public bool TelemetryStatus { get; set; } + [KeyName("utc_timestamps")] public bool TelemetryUseUtcTimeStamps { get; set; } + + [KeyName("telemetry_file_dir")] public string? TelemetryFileDir { get; set; } + + [KeyName("telemetry_server_url")] public string? TelemetryServerUrl { get; set; } + + [KeyName("include_hooks")] public bool TelemetryIncludeHooks { get; set; } + [KeyName("active_app")] public bool AppTelemetryStatus { get; set; } + + [KeyName("apptelemetry_server_url")] public string? AppTelemetryServerUrl { get; set; } + + [KeyName("apptelemetry_event_flags")] public int AppTelemetryEventFlags { get; set; } } \ No newline at end of file From 6baf57d9779ed6107a0f2497a73f280afe45405a Mon Sep 17 00:00:00 2001 From: dosymep Date: Fri, 27 Dec 2024 11:43:32 +0300 Subject: [PATCH 19/37] add save config sections --- .../ConfigurationBase.cs | 3 +- .../ConfigurationService.cs | 83 +++++++++++-------- .../pyRevitLabs.Configurations/CoreSection.cs | 37 +++++---- .../RoutesSection.cs | 6 +- .../TelemetrySection.cs | 10 +-- 5 files changed, 77 insertions(+), 62 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs index abd8770ea..498736b6e 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs @@ -108,9 +108,10 @@ public T GetValue(string sectionName, string keyName) return (T) GetValueImpl(typeof(T), sectionName, keyName); } + /// public object GetValue(Type typeObject, string sectionName, string keyName) { - throw new NotImplementedException(); + return GetValueImpl(typeObject, sectionName, keyName); } /// diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index 2f1d8e369..2cb08c796 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using pyRevitLabs.Configurations.Abstractions; using pyRevitLabs.Configurations.Attributes; @@ -29,71 +30,83 @@ internal static IConfigurationService Create( } public IEnumerable ConfigurationNames => _configurations.Keys; - public IEnumerable Configurations => _configurations.Values; + + public IEnumerable Configurations => _names + .Select(item => _configurations[item.Name!]) + .ToArray(); public CoreSection? Core { get; private set; } public RoutesSection? Routes { get; private set; } public TelemetrySection? Telemetry { get; private set; } - + public void LoadConfigurations() { - Core = GetConfiguration(); - Routes = GetConfiguration(); - Telemetry = GetConfiguration(); + Core = GetSection(); + Routes = GetSection(); + Telemetry = GetSection(); } - internal T GetConfiguration() + public void SaveSection(string configurationName, T sectionValue) { - return GetConfigurationImpl(DefaultConfigurationName); - } + if (sectionValue is null) + throw new ArgumentNullException(nameof(sectionValue)); - internal T GetConfiguration(string configurationName) - { - return GetConfigurationImpl(configurationName); - } + if (string.IsNullOrWhiteSpace(configurationName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(configurationName)); - internal T? GetConfigurationOrDefault(T? defaultValue = default) - { - return GetConfigurationOrDefault(DefaultConfigurationName, defaultValue); + if (!_configurations.TryGetValue(configurationName, out IConfiguration? configuration)) + throw new ArgumentException($"Configuration with name {configurationName} not found"); + + Type configurationType = typeof(T); + SaveSection(configurationType, sectionValue, configuration); } - internal T? GetConfigurationOrDefault(string configurationName, T? defaultValue = default) + private static void SaveSection(Type configurationType, object? sectionValue, IConfiguration configuration) { - try - { - return GetConfigurationImpl(configurationName); - } - catch + string sectionName = + GetCustomAttribute(configurationType)?.SectionName ?? configurationType.Name; + + foreach (var propertyInfo in configurationType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { - return defaultValue; + string keyName = GetCustomAttribute(propertyInfo)?.KeyName ?? propertyInfo.Name; + object? keyValue = propertyInfo.GetValue(sectionValue); + if (keyValue is not null) + configuration.SetValue(sectionName, keyName, keyValue); } } - private T GetConfigurationImpl(string configurationName) + internal T GetSection() { - if (string.IsNullOrWhiteSpace(configurationName)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(configurationName)); - - if (!_configurations.TryGetValue(configurationName, out var configuration)) - throw new ArgumentException($"Configuration with name {configurationName} not found"); - Type configurationType = typeof(T); + return (T) CreateSection(configurationType, Configurations.ToArray()); + } + private static object CreateSection(Type configurationType, params IConfiguration[] configurations) + { string sectionName = GetCustomAttribute(configurationType)?.SectionName ?? configurationType.Name; - var sectionConfiguration = Activator.CreateInstance(); + var sectionConfiguration = Activator.CreateInstance(configurationType); + foreach (var propertyInfo in configurationType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { string keyName = GetCustomAttribute(propertyInfo)?.KeyName ?? propertyInfo.Name; - object? keyValue = configuration.GetValueOrDefault( - propertyInfo.PropertyType, sectionName, keyName, propertyInfo.GetValue(sectionConfiguration)); - - propertyInfo.SetValue(sectionConfiguration, keyValue); + object? keyValue = GetKeyValue(configurations, propertyInfo, sectionName, keyName); + propertyInfo.SetValue(sectionConfiguration, keyValue ?? propertyInfo.GetValue(sectionConfiguration)); } - return sectionConfiguration; + return sectionConfiguration!; + } + + private static object? GetKeyValue( + IEnumerable configurations, + PropertyInfo propertyInfo, + string sectionName, string keyName) + { + return configurations + .Select(item=> item.GetValueOrDefault(propertyInfo.PropertyType, sectionName, keyName)) + .FirstOrDefault(item => item != default); } private static T? GetCustomAttribute(MemberInfo memberInfo) where T : Attribute diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs index b46f95ada..92ab67fb6 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs @@ -1,4 +1,5 @@ -using pyRevitLabs.Configurations.Attributes; +using pyRevitLabs.Configurations.Abstractions; +using pyRevitLabs.Configurations.Attributes; namespace pyRevitLabs.Configurations; @@ -13,55 +14,55 @@ public enum LogLevels public sealed record CoreSection { [KeyName("bincache")] - public bool BinCache { get; set; } + public bool? BinCache { get; set; } [KeyName("loadbeta")] - public bool LoadBeta { get; set; } + public bool? LoadBeta { get; set; } [KeyName("autoupdate")] - public bool AutoUpdate { get; set; } + public bool? AutoUpdate { get; set; } [KeyName("checkupdates")] - public bool CheckUpdates { get; set; } + public bool? CheckUpdates { get; set; } [KeyName("usercanupdate")] - public bool UserCanUpdate { get; set; } = true; + public bool? UserCanUpdate { get; set; } = true; [KeyName("usercanextend")] - public bool UserCanExtend { get; set; } = true; + public bool? UserCanExtend { get; set; } = true; [KeyName("usercanconfig")] - public bool UserCanConfig { get; set; } = true; + public bool? UserCanConfig { get; set; } = true; [KeyName("rocketmode")] - public bool RocketMode { get; set; } = true; + public bool? RocketMode { get; set; } = true; [KeyName("user_locale")] - public string UserLocale { get; set; } = "en_us"; + public string? UserLocale { get; set; } = "en_us"; [KeyName("log_level")] - public LogLevels LogLevel { get; set; } + public LogLevels? LogLevel { get; set; } [KeyName("filelogging")] - public bool FileLogging { get; set; } + public bool? FileLogging { get; set; } [KeyName("startuplogtimeout")] - public int StartupLogTimeout { get; set; } = 10; + public int? StartupLogTimeout { get; set; } = 10; [KeyName("cpyengine")] - public int CpythonEngineVersion { get; set; } + public int? CpythonEngineVersion { get; set; } [KeyName("requiredhostbuild")] - public bool RequiredHostBuild { get; set; } + public bool? RequiredHostBuild { get; set; } [KeyName("minhostdrivefreespace")] - public long MinHostDriveFreeSpace { get; set; } + public long? MinHostDriveFreeSpace { get; set; } [KeyName("colorize_docs")] - public bool ColorizeDocs { get; set; } + public bool? ColorizeDocs { get; set; } [KeyName("tooltip_debug_info")] - public bool TooltipDebugInfo { get; set; } + public bool? TooltipDebugInfo { get; set; } [KeyName("outputstylesheet")] public string? OutputStylesheet { get; set; } diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs index e61614dd1..f1ff7d360 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/RoutesSection.cs @@ -6,14 +6,14 @@ namespace pyRevitLabs.Configurations; public sealed record RoutesSection { [KeyName("enabled")] - public bool Status { get; set; } + public bool? Status { get; set; } [KeyName("host")] public string? Host { get; set; } [KeyName("port")] - public int Port { get; set; } = 48884; + public int? Port { get; set; } = 48884; [KeyName("core_api")] - public bool LoadCoreApi { get; set; } + public bool? LoadCoreApi { get; set; } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs index 03c5dd173..daa4bcadf 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/TelemetrySection.cs @@ -6,10 +6,10 @@ namespace pyRevitLabs.Configurations; public record TelemetrySection { [KeyName("active")] - public bool TelemetryStatus { get; set; } + public bool? TelemetryStatus { get; set; } [KeyName("utc_timestamps")] - public bool TelemetryUseUtcTimeStamps { get; set; } + public bool? TelemetryUseUtcTimeStamps { get; set; } [KeyName("telemetry_file_dir")] public string? TelemetryFileDir { get; set; } @@ -18,14 +18,14 @@ public record TelemetrySection public string? TelemetryServerUrl { get; set; } [KeyName("include_hooks")] - public bool TelemetryIncludeHooks { get; set; } + public bool? TelemetryIncludeHooks { get; set; } [KeyName("active_app")] - public bool AppTelemetryStatus { get; set; } + public bool? AppTelemetryStatus { get; set; } [KeyName("apptelemetry_server_url")] public string? AppTelemetryServerUrl { get; set; } [KeyName("apptelemetry_event_flags")] - public int AppTelemetryEventFlags { get; set; } + public int? AppTelemetryEventFlags { get; set; } } \ No newline at end of file From cf74632834964b9af62c6a8a8bed96db73dcee1c Mon Sep 17 00:00:00 2001 From: dosymep Date: Fri, 27 Dec 2024 11:46:43 +0300 Subject: [PATCH 20/37] update public interface --- .../Abstractions/IConfigurationService.cs | 7 ++++++- .../ConfigurationService.cs | 17 ++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs index 5c2c3448b..c4b7bfbdd 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs @@ -2,5 +2,10 @@ public interface IConfigurationService { - + CoreSection? Core { get; } + RoutesSection? Routes { get; } + TelemetrySection? Telemetry { get; } + + T GetSection(); + void SaveSection(string configurationName, T sectionValue); } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index 2cb08c796..34393ab5e 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -20,6 +20,9 @@ internal ConfigurationService( { _names = names; _configurations = configurations; + + // TODO: Change behavior + LoadConfigurations(); } internal static IConfigurationService Create( @@ -39,13 +42,19 @@ internal static IConfigurationService Create( public RoutesSection? Routes { get; private set; } public TelemetrySection? Telemetry { get; private set; } - public void LoadConfigurations() + private void LoadConfigurations() { Core = GetSection(); Routes = GetSection(); Telemetry = GetSection(); } + public T GetSection() + { + Type configurationType = typeof(T); + return (T) CreateSection(configurationType, Configurations.ToArray()); + } + public void SaveSection(string configurationName, T sectionValue) { if (sectionValue is null) @@ -75,12 +84,6 @@ private static void SaveSection(Type configurationType, object? sectionValue, IC } } - internal T GetSection() - { - Type configurationType = typeof(T); - return (T) CreateSection(configurationType, Configurations.ToArray()); - } - private static object CreateSection(Type configurationType, params IConfiguration[] configurations) { string sectionName = From 6122fd911a43c4ed6ea3dbd1bcfaad07e9068a3a Mon Sep 17 00:00:00 2001 From: dosymep Date: Fri, 27 Dec 2024 15:13:15 +0300 Subject: [PATCH 21/37] update console config command --- dev/pyRevitLabs/pyRevitCLI/PyRevitCLI.cs | 91 ++-- .../pyRevitCLI/Resources/UsagePatterns.txt | 54 +- .../Abstractions/IConfigurationService.cs | 9 +- .../ConfigurationService.cs | 49 +- .../pyRevitLabs.Configurations/CoreSection.cs | 13 +- .../pyRevitLabs.PyRevit/PyRevitConfig.cs | 112 ---- .../pyRevitLabs.PyRevit/PyRevitConfigs.cs | 503 ++++++++++-------- 7 files changed, 409 insertions(+), 422 deletions(-) delete mode 100644 dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs diff --git a/dev/pyRevitLabs/pyRevitCLI/PyRevitCLI.cs b/dev/pyRevitLabs/pyRevitCLI/PyRevitCLI.cs index 0ece364cc..9f1d52761 100644 --- a/dev/pyRevitLabs/pyRevitCLI/PyRevitCLI.cs +++ b/dev/pyRevitLabs/pyRevitCLI/PyRevitCLI.cs @@ -599,12 +599,14 @@ private static void ProcessArguments() { } else if (all("configs")) { + string revitVersion = TryGetValue(""); + if (IsHelpMode) PyRevitCLIAppHelps.PrintHelp(PyRevitCLICommandType.Configs); else if (all("bincache")) { if (any("enable", "disable")) - PyRevitConfigs.SetBinaryCaches(arguments["enable"].IsTrue); + PyRevitConfigs.SetBinaryCaches(arguments["enable"].IsTrue, revitVersion); else Console.WriteLine(string.Format("Binary cache is {0}", PyRevitConfigs.GetBinaryCaches() ? "Enabled" : "Disabled")); @@ -612,7 +614,7 @@ private static void ProcessArguments() { else if (all("checkupdates")) { if (any("enable", "disable")) - PyRevitConfigs.SetCheckUpdates(arguments["enable"].IsTrue); + PyRevitConfigs.SetCheckUpdates(arguments["enable"].IsTrue, revitVersion); else Console.WriteLine(string.Format("Check Updates is {0}", PyRevitConfigs.GetCheckUpdates() ? "Enabled" : "Disabled")); @@ -620,7 +622,7 @@ private static void ProcessArguments() { else if (all("autoupdate")) { if (any("enable", "disable")) - PyRevitConfigs.SetAutoUpdate(arguments["enable"].IsTrue); + PyRevitConfigs.SetAutoUpdate(arguments["enable"].IsTrue, revitVersion); else Console.WriteLine(string.Format("Auto Update is {0}", PyRevitConfigs.GetAutoUpdate() ? "Enabled" : "Disabled")); @@ -628,7 +630,7 @@ private static void ProcessArguments() { else if (all("rocketmode")) { if (any("enable", "disable")) - PyRevitConfigs.SetRocketMode(arguments["enable"].IsTrue); + PyRevitConfigs.SetRocketMode(arguments["enable"].IsTrue, revitVersion); else Console.WriteLine(string.Format("Rocket Mode is {0}", PyRevitConfigs.GetRocketMode() ? "Enabled" : "Disabled")); @@ -636,13 +638,13 @@ private static void ProcessArguments() { else if (all("logs")) { if (all("none")) - PyRevitConfigs.SetLoggingLevel(PyRevitLogLevels.Quiet); + PyRevitConfigs.SetLoggingLevel(PyRevitLogLevels.Quiet, revitVersion); else if (all("verbose")) - PyRevitConfigs.SetLoggingLevel(PyRevitLogLevels.Verbose); + PyRevitConfigs.SetLoggingLevel(PyRevitLogLevels.Verbose, revitVersion); else if (all("debug")) - PyRevitConfigs.SetLoggingLevel(PyRevitLogLevels.Debug); + PyRevitConfigs.SetLoggingLevel(PyRevitLogLevels.Debug, revitVersion); else Console.WriteLine(string.Format("Logging Level is {0}", PyRevitConfigs.GetLoggingLevel().ToString())); @@ -650,7 +652,7 @@ private static void ProcessArguments() { else if (all("filelogging")) { if (any("enable", "disable")) - PyRevitConfigs.SetFileLogging(arguments["enable"].IsTrue); + PyRevitConfigs.SetFileLogging(arguments["enable"].IsTrue, revitVersion); else Console.WriteLine(string.Format("File Logging is {0}", PyRevitConfigs.GetFileLogging() ? "Enabled" : "Disabled")); @@ -661,12 +663,12 @@ private static void ProcessArguments() { Console.WriteLine(string.Format("Startup log timeout is set to: {0}", PyRevitConfigs.GetStartupLogTimeout())); else - PyRevitConfigs.SetStartupLogTimeout(int.Parse(TryGetValue(""))); + PyRevitConfigs.SetStartupLogTimeout(int.Parse(TryGetValue("")), revitVersion); } else if (all("loadbeta")) { if (any("enable", "disable")) - PyRevitConfigs.SetLoadBetaTools(arguments["enable"].IsTrue); + PyRevitConfigs.SetLoadBetaTools(arguments["enable"].IsTrue, revitVersion); else Console.WriteLine(string.Format("Load Beta is {0}", PyRevitConfigs.GetLoadBetaTools() ? "Enabled" : "Disabled")); @@ -677,12 +679,12 @@ private static void ProcessArguments() { Console.WriteLine(string.Format("CPython version is set to: {0}", PyRevitConfigs.GetCpythonEngineVersion())); else - PyRevitConfigs.SetCpythonEngineVersion(int.Parse(TryGetValue(""))); + PyRevitConfigs.SetCpythonEngineVersion(int.Parse(TryGetValue("")), revitVersion); } else if (all("usercanupdate")) { if (any("yes", "no")) - PyRevitConfigs.SetUserCanUpdate(arguments["yes"].IsTrue); + PyRevitConfigs.SetUserCanUpdate(arguments["yes"].IsTrue, revitVersion); else Console.WriteLine(string.Format("User {0} update", PyRevitConfigs.GetUserCanUpdate() ? "CAN" : "CAN NOT")); @@ -690,7 +692,7 @@ private static void ProcessArguments() { else if (all("usercanextend")) { if (any("yes", "no")) - PyRevitConfigs.SetUserCanExtend(arguments["yes"].IsTrue); + PyRevitConfigs.SetUserCanExtend(arguments["yes"].IsTrue, revitVersion); else Console.WriteLine(string.Format("User {0} extend", PyRevitConfigs.GetUserCanExtend() ? "CAN" : "CAN NOT")); @@ -698,7 +700,7 @@ private static void ProcessArguments() { else if (all("usercanconfig")) { if (any("yes", "no")) - PyRevitConfigs.SetUserCanConfig(arguments["yes"].IsTrue); + PyRevitConfigs.SetUserCanConfig(arguments["yes"].IsTrue, revitVersion); else Console.WriteLine(string.Format("User {0} config", PyRevitConfigs.GetUserCanConfig() ? "CAN" : "CAN NOT")); @@ -707,7 +709,7 @@ private static void ProcessArguments() { else if (all("colordocs")) { if (any("enable", "disable")) - PyRevitConfigs.SetColorizeDocs(arguments["enable"].IsTrue); + PyRevitConfigs.SetColorizeDocs(arguments["enable"].IsTrue, revitVersion); else Console.WriteLine(string.Format("Doc Colorizer is {0}", PyRevitConfigs.GetColorizeDocs() ? "Enabled" : "Disabled")); @@ -715,7 +717,7 @@ private static void ProcessArguments() { else if (all("tooltipdebuginfo")) { if (any("enable", "disable")) - PyRevitConfigs.SetAppendTooltipEx(arguments["enable"].IsTrue); + PyRevitConfigs.SetAppendTooltipEx(arguments["enable"].IsTrue, revitVersion); else Console.WriteLine(string.Format("Doc Colorizer is {0}", PyRevitConfigs.GetAppendTooltipEx() ? "Enabled" : "Disabled")); @@ -728,24 +730,24 @@ private static void ProcessArguments() { Console.WriteLine(string.Format("Routes Port: {0}", PyRevitConfigs.GetRoutesServerPort())); } else - PyRevitConfigs.SetRoutesServerPort(int.Parse(portNumber)); + PyRevitConfigs.SetRoutesServerPort(int.Parse(portNumber), revitVersion); } else if (all("coreapi")) { if (all("enable")) - PyRevitConfigs.SetRoutesLoadCoreAPIStatus(true); + PyRevitConfigs.SetRoutesLoadCoreAPIStatus(true, revitVersion); else if (all("disable")) - PyRevitConfigs.SetRoutesLoadCoreAPIStatus(false); + PyRevitConfigs.SetRoutesLoadCoreAPIStatus(false, revitVersion); else Console.WriteLine(string.Format("Routes Core API is {0}", PyRevitConfigs.GetRoutesLoadCoreAPIStatus() ? "Enabled" : "Disabled")); } else if (all("enable")) - PyRevitConfigs.EnableRoutesServer(); + PyRevitConfigs.EnableRoutesServer(revitVersion); else if (all("disable")) - PyRevitConfigs.DisableRoutesServer(); + PyRevitConfigs.DisableRoutesServer(revitVersion); else { Console.WriteLine(string.Format("Routes Server is {0}", @@ -756,7 +758,7 @@ private static void ProcessArguments() { else if (all("telemetry")) { if (all("utc")) { if (any("yes", "no")) - PyRevitConfigs.SetUTCStamps(arguments["yes"].IsTrue); + PyRevitConfigs.SetUTCStamps(arguments["yes"].IsTrue, revitVersion); else Console.WriteLine(PyRevitConfigs.GetUTCStamps() ? "Using UTC timestamps" : "Using Local timestamps"); } @@ -766,7 +768,7 @@ private static void ProcessArguments() { if (destPath is null) Console.WriteLine(string.Format("Telemetry File Path: {0}", PyRevitConfigs.GetAppTelemetryFlags())); else - PyRevitConfigs.EnableTelemetry(telemetryFileDir: destPath); + PyRevitConfigs.EnableTelemetry(telemetryFileDir: destPath, revitVersion: revitVersion); } else if (all("server")) { @@ -774,22 +776,22 @@ private static void ProcessArguments() { if (serverUrl is null) Console.WriteLine(string.Format("Telemetry Server Url: {0}", PyRevitConfigs.GetAppTelemetryFlags())); else - PyRevitConfigs.EnableTelemetry(telemetryServerUrl: serverUrl); + PyRevitConfigs.EnableTelemetry(telemetryServerUrl: serverUrl, revitVersion: revitVersion); } else if (all("hooks")) { if (any("yes", "no")) - PyRevitConfigs.SetTelemetryIncludeHooks(arguments["yes"].IsTrue); + PyRevitConfigs.SetTelemetryIncludeHooks(arguments["yes"].IsTrue, revitVersion); else Console.WriteLine(PyRevitConfigs.GetTelemetryIncludeHooks() ? "Sending telemetry for hooks" : "Not sending telemetry for hooks"); } else if (all("enable")) - PyRevitConfigs.EnableTelemetry(); + PyRevitConfigs.EnableTelemetry(revitVersion: revitVersion); else if (all("disable")) - PyRevitConfigs.DisableTelemetry(); + PyRevitConfigs.DisableTelemetry(revitVersion: revitVersion); else { Console.WriteLine(string.Format("Telemetry is {0}", @@ -805,7 +807,7 @@ private static void ProcessArguments() { if (flagsValue is null) Console.WriteLine(string.Format("App Telemetry Flags: {0}", PyRevitConfigs.GetAppTelemetryFlags())); else - PyRevitConfigs.SetAppTelemetryFlags(flags: flagsValue); + PyRevitConfigs.SetAppTelemetryFlags(flags: flagsValue, revitVersion: revitVersion); } else if (all("server")) { @@ -813,15 +815,15 @@ private static void ProcessArguments() { if (serverPath is null) Console.WriteLine(string.Format("App Telemetry Server: {0}", PyRevitConfigs.GetAppTelemetryServerUrl())); else - PyRevitConfigs.EnableAppTelemetry(apptelemetryServerUrl: serverPath); + PyRevitConfigs.EnableAppTelemetry(apptelemetryServerUrl: serverPath, revitVersion: revitVersion); } else if (all("enable")) - PyRevitConfigs.EnableAppTelemetry(); + PyRevitConfigs.EnableAppTelemetry(revitVersion); else if (all("disable")) - PyRevitConfigs.DisableAppTelemetry(); + PyRevitConfigs.DisableAppTelemetry(revitVersion); else { Console.WriteLine(string.Format("App Telemetry is {0}", @@ -837,7 +839,7 @@ private static void ProcessArguments() { Console.WriteLine(string.Format("Output Style Sheet is set to: {0}", PyRevitConfigs.GetOutputStyleSheet())); else - PyRevitConfigs.SetOutputStyleSheet(TryGetValue("")); + PyRevitConfigs.SetOutputStyleSheet(TryGetValue(""), revitVersion); } else if (all("seed")) @@ -847,12 +849,14 @@ private static void ProcessArguments() { if (arguments[""] != null) { // extract section and option names string orignalOptionValue = TryGetValue(""); - if (orignalOptionValue.Split(':').Count() == 2) { + if (orignalOptionValue.Split(':').Count() == 2) + { string configSection = orignalOptionValue.Split(':')[0]; string configOption = orignalOptionValue.Split(':')[1]; - var cfg = PyRevitConfigs.GetConfigFile(); - cfg.SetValue(configSection, configOption, arguments["enable"].IsTrue); + var cfg = PyRevitConfigs.GetConfigFile(revitVersion); + cfg.SetSectionKeyValue( + revitVersion, configSection, configOption, arguments["enable"].IsTrue); } else PyRevitCLIAppHelps.PrintHelp(PyRevitCLICommandType.Main); @@ -867,18 +871,19 @@ private static void ProcessArguments() { string configSection = orignalOptionValue.Split(':')[0]; string configOption = orignalOptionValue.Split(':')[1]; - var cfg = PyRevitConfigs.GetConfigFile(); + var cfg = PyRevitConfigs.GetConfigFile(revitVersion); // if no value provided, read the value var optValue = TryGetValue(""); - if (optValue != null) - cfg.SetValue(configSection, configOption, optValue); - else if (optValue is null) { - var existingVal = cfg.GetValue(configSection, configOption); - if (existingVal != null) - Console.WriteLine( string.Format("{0} = {1}", configOption, existingVal)); + if (optValue is not null) + cfg.SetSectionKeyValue(revitVersion, configSection, configOption, optValue); + else + { + var existingVal = cfg.GetSectionKeyValueOrDefault(revitVersion, configSection, configOption); + if (!string.IsNullOrEmpty(existingVal)) + Console.WriteLine($"{configOption} = {existingVal}"); else - Console.WriteLine(string.Format("Configuration key \"{0}\" is not set", configOption)); + Console.WriteLine($"Configuration key \"{configOption}\" is not set"); } } else diff --git a/dev/pyRevitLabs/pyRevitCLI/Resources/UsagePatterns.txt b/dev/pyRevitLabs/pyRevitCLI/Resources/UsagePatterns.txt index 651714fd7..a93b0aaaf 100644 --- a/dev/pyRevitLabs/pyRevitCLI/Resources/UsagePatterns.txt +++ b/dev/pyRevitLabs/pyRevitCLI/Resources/UsagePatterns.txt @@ -74,36 +74,36 @@ Usage: pyrevit config (-h | --help) pyrevit config --from= [--log=] pyrevit configs (-h | --help) - pyrevit configs bincache [(enable | disable)] [--log=] - pyrevit configs checkupdates [(enable | disable)] [--log=] - pyrevit configs autoupdate [(enable | disable)] [--log=] - pyrevit configs rocketmode [(enable | disable)] [--log=] - pyrevit configs logs [(none | verbose | debug)] [--log=] - pyrevit configs filelogging [(enable | disable)] [--log=] - pyrevit configs startuptimeout [] [--log=] - pyrevit configs loadbeta [(enable | disable)] [--log=] - pyrevit configs cpyversion [] [--log=] - pyrevit configs usercanupdate [(yes | no)] [--log=] - pyrevit configs usercanextend [(yes | no)] [--log=] - pyrevit configs usercanconfig [(yes | no)] [--log=] - pyrevit configs colordocs [(enable | disable)] [--log=] - pyrevit configs tooltipdebuginfo [(enable | disable)] [--log=] + pyrevit configs bincache [(enable | disable)] [] [--log=] + pyrevit configs checkupdates [(enable | disable)] [] [--log=] + pyrevit configs autoupdate [(enable | disable)] [] [--log=] + pyrevit configs rocketmode [(enable | disable)] [] [--log=] + pyrevit configs logs [(none | verbose | debug)] [] [--log=] + pyrevit configs filelogging [(enable | disable)] [] [--log=] + pyrevit configs startuptimeout [] [] [--log=] + pyrevit configs loadbeta [(enable | disable)] [] [--log=] + pyrevit configs cpyversion [] [] [--log=] + pyrevit configs usercanupdate [(yes | no)] [] [--log=] + pyrevit configs usercanextend [(yes | no)] [] [--log=] + pyrevit configs usercanconfig [(yes | no)] [] [--log=] + pyrevit configs colordocs [(enable | disable)] [] [--log=] + pyrevit configs tooltipdebuginfo [(enable | disable)] [] [--log=] pyrevit configs routes [(-h | --help)] - pyrevit configs routes [(enable | disable)] [--log=] - pyrevit configs routes port [] [--log=] - pyrevit configs routes coreapi [(enable | disable)] [--log=] + pyrevit configs routes [(enable | disable)] [] [--log=] + pyrevit configs routes port [] [] [--log=] + pyrevit configs routes coreapi [(enable | disable)] [] [--log=] pyrevit configs telemetry [(-h | --help)] - pyrevit configs telemetry [(enable | disable)] [--log=] - pyrevit configs telemetry utc [(yes | no)] [--log=] - pyrevit configs telemetry (file | server) [] [--log=] - pyrevit configs telemetry hooks [(yes | no)] [--log=] - pyrevit configs apptelemetry [(enable | disable)] [--log=] - pyrevit configs apptelemetry flags [] [--log=] - pyrevit configs apptelemetry server [] [--log=] - pyrevit configs outputcss [] [--log=] + pyrevit configs telemetry [(enable | disable)] [] [--log=] + pyrevit configs telemetry utc [(yes | no)] [] [--log=] + pyrevit configs telemetry (file | server) [] [] [--log=] + pyrevit configs telemetry hooks [(yes | no)] [] [--log=] + pyrevit configs apptelemetry [(enable | disable)] [] [--log=] + pyrevit configs apptelemetry flags [] [] [--log=] + pyrevit configs apptelemetry server [] [] [--log=] + pyrevit configs outputcss [] [] [--log=] pyrevit configs seed [--lock] [--log=] - pyrevit configs [(enable | disable)] [--log=] - pyrevit configs [] [--log=] + pyrevit configs [(enable | disable)] [] [--log=] + pyrevit configs [] [] [--log=] pyrevit doctor (-h | --help) pyrevit doctor [--list] pyrevit doctor [--dryrun] diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs index c4b7bfbdd..f9434521e 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs @@ -2,10 +2,13 @@ public interface IConfigurationService { - CoreSection? Core { get; } - RoutesSection? Routes { get; } - TelemetrySection? Telemetry { get; } + CoreSection Core { get; } + RoutesSection Routes { get; } + TelemetrySection Telemetry { get; } T GetSection(); void SaveSection(string configurationName, T sectionValue); + + void SetSectionKeyValue(string configurationName, string sectionName, string keyName, T keyValue); + T? GetSectionKeyValueOrDefault(string configurationName, string sectionName, string keyName, T? defaultValue = default); } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index 34393ab5e..6a315192f 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -12,7 +12,7 @@ public sealed class ConfigurationService : IConfigurationService private readonly List _names; private readonly IDictionary _configurations; - public static readonly string DefaultConfigurationName = "Default"; + public const string DefaultConfigurationName = "Default"; internal ConfigurationService( List names, @@ -38,9 +38,9 @@ internal static IConfigurationService Create( .Select(item => _configurations[item.Name!]) .ToArray(); - public CoreSection? Core { get; private set; } - public RoutesSection? Routes { get; private set; } - public TelemetrySection? Telemetry { get; private set; } + public CoreSection Core { get; private set; } = new(); + public RoutesSection Routes { get; private set; } = new(); + public TelemetrySection Telemetry { get; private set; } = new(); private void LoadConfigurations() { @@ -70,6 +70,47 @@ public void SaveSection(string configurationName, T sectionValue) SaveSection(configurationType, sectionValue, configuration); } + public void SetSectionKeyValue(string configurationName, string sectionName, string keyName, T keyValue) + { + if (keyValue == null) + throw new ArgumentNullException(nameof(keyValue)); + + if (string.IsNullOrEmpty(configurationName)) + throw new ArgumentException("Value cannot be null or empty.", nameof(configurationName)); + + if (string.IsNullOrEmpty(sectionName)) + throw new ArgumentException("Value cannot be null or empty.", nameof(sectionName)); + + if (string.IsNullOrEmpty(keyName)) + throw new ArgumentException("Value cannot be null or empty.", nameof(keyName)); + + if (!_configurations.TryGetValue(configurationName, out IConfiguration? configuration)) + throw new ArgumentException($"Configuration with name {configurationName} not found"); + + configuration.SetValue(sectionName, keyName, keyValue); + } + + public T? GetSectionKeyValueOrDefault( + string configurationName, + string sectionName, + string keyName, + T? defaultValue = default) + { + if (string.IsNullOrEmpty(configurationName)) + throw new ArgumentException("Value cannot be null or empty.", nameof(configurationName)); + + if (string.IsNullOrEmpty(sectionName)) + throw new ArgumentException("Value cannot be null or empty.", nameof(sectionName)); + + if (string.IsNullOrEmpty(keyName)) + throw new ArgumentException("Value cannot be null or empty.", nameof(keyName)); + + if (!_configurations.TryGetValue(configurationName, out IConfiguration? configuration)) + throw new ArgumentException($"Configuration with name {configurationName} not found"); + + return configuration.GetValue(sectionName, keyName); + } + private static void SaveSection(Type configurationType, object? sectionValue, IConfiguration configuration) { string sectionName = diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs index 92ab67fb6..496557c96 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs @@ -39,9 +39,12 @@ public sealed record CoreSection [KeyName("user_locale")] public string? UserLocale { get; set; } = "en_us"; - - [KeyName("log_level")] - public LogLevels? LogLevel { get; set; } + + [KeyName("debug")] + public bool? Debug { get; set; } + + [KeyName("verbose")] + public bool? Verbose { get; set; } [KeyName("filelogging")] public bool? FileLogging { get; set; } @@ -53,7 +56,7 @@ public sealed record CoreSection public int? CpythonEngineVersion { get; set; } [KeyName("requiredhostbuild")] - public bool? RequiredHostBuild { get; set; } + public string? RequiredHostBuild { get; set; } [KeyName("minhostdrivefreespace")] public long? MinHostDriveFreeSpace { get; set; } @@ -65,5 +68,5 @@ public sealed record CoreSection public bool? TooltipDebugInfo { get; set; } [KeyName("outputstylesheet")] - public string? OutputStylesheet { get; set; } + public string? OutputStyleSheet { get; set; } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs deleted file mode 100644 index 7b44c4f08..000000000 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfig.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -using pyRevitLabs.Configurations.Abstractions; -using pyRevitLabs.NLog; - - -namespace pyRevitLabs.PyRevit -{ - public class PyRevitConfig - { - private readonly IConfiguration _configuration; - private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); - - public PyRevitConfig(IConfiguration configuration) - { - _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - } - - /// - /// Get config key value - /// - /// - /// - /// - public string GetValue(string sectionName, string keyName) - { - _logger.Debug("Try getting config value \"{@SectionName}:{@KeyName}\"", sectionName, keyName); - return _configuration.GetValueOrDefault(sectionName, keyName); - } - - /// - /// Get config key value and make a string list out of it - /// - /// - /// - /// - public List GetListValue(string sectionName, string keyName) - { - _logger.Debug("Try getting config as list value \"{SectionName}:{KeyName}\"", sectionName, keyName); - return _configuration.GetValueOrDefault(sectionName, keyName, new List()); - } - - /// - /// Get config key value and make a string dictionary out of it - /// - /// - /// - /// - public Dictionary GetDictValue(string sectionName, string keyName) - { - _logger.Debug("Try getting config as dict value \"{SectionName}:{KeyName}\"", sectionName, keyName); - return _configuration.GetValueOrDefault(sectionName, keyName, new Dictionary()); - } - - /// - /// Set config key value, creates the config if not set yet - /// - /// - /// - /// - public void SetValue(string sectionName, string keyName, string value) - { - _configuration.SetValue(sectionName, keyName, value); - } - - /// - /// Sets config key value as bool - /// - /// - /// - /// - public void SetValue(string sectionName, string keyName, bool value) - { - _configuration.SetValue(sectionName, keyName, value); - } - - /// - /// Sets config key value as int - /// - /// - /// - /// - public void SetValue(string sectionName, string keyName, int value) - { - _configuration.SetValue(sectionName, keyName, value); - } - - /// - /// Sets config key value as string list - /// - /// - /// - /// - public void SetValue(string sectionName, string keyName, IEnumerable value) - { - _configuration.SetValue(sectionName, keyName, value.ToArray()); - } - - /// - /// Sets config key value as string dictionary - /// - /// - /// - /// - public void SetValue(string sectionName, string keyName, IDictionary dictString) - { - _configuration.SetValue(sectionName, keyName, dictString); - } - } -} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs index d4512313d..e7e713265 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs @@ -27,7 +27,8 @@ public static class PyRevitConfigs /// Returns config file. /// /// Returns admin config if admin config exists and user config not found. - public static PyRevitConfig GetConfigFile(string overrideName = default) + public static IConfigurationService GetConfigFile( + string overrideName = ConfigurationService.DefaultConfigurationName) { // make sure the file exists and if not create an empty one string userConfig = PyRevitConsts.ConfigFilePath; @@ -36,11 +37,11 @@ public static PyRevitConfig GetConfigFile(string overrideName = default) if (!File.Exists(userConfig) && File.Exists(adminConfig)) { - _logger.Info("Creating admin config {@ConfigPath}", adminConfig); + _logger.Info("Creating admin config {@ConfigPath}...", adminConfig); return CreateConfiguration(adminConfig, true, overrideName); } - _logger.Info("Creating user config {@ConfigPath}", userConfig); + _logger.Info("Creating user config {@ConfigPath}...", userConfig); return CreateConfiguration(userConfig, false, overrideName); } @@ -52,8 +53,8 @@ public static void DeleteConfig() { if (!File.Exists(PyRevitConsts.ConfigFilePath)) return; - _logger.Info("Deleting config {@ConfigPath}", PyRevitConsts.ConfigFilePath); - + _logger.Info("Deleting config {@ConfigPath}...", PyRevitConsts.ConfigFilePath); + try { File.Delete(PyRevitConsts.ConfigFilePath); @@ -122,14 +123,14 @@ public static void SetupConfig(string templateConfigFilePath = null) } catch (Exception ex) { - throw new PyRevitException($"Failed configuring config file from template at {sourceFile}", ex); + throw new PyRevitException($"Failed configuring config file from template at {sourceFile}...", ex); } } - private static PyRevitConfig CreateConfiguration( + private static IConfigurationService CreateConfiguration( string configPath, bool readOnly = false, - string overrideName = default) + string overrideName = ConfigurationService.DefaultConfigurationName) { var builder = new ConfigurationBuilder() .AddIniConfiguration(configPath, "default", readOnly); @@ -138,477 +139,523 @@ private static PyRevitConfig CreateConfiguration( { builder.AddIniConfiguration( Path.ChangeExtension(configPath, - $"{overrideName}.{IniConfiguration.DefaultFileExtension}"), overrideName!); + $"{overrideName}.{IniConfiguration.DefaultFileExtension}"), overrideName); } - var configuration = builder.Build(); - return new PyRevitConfig(configuration); + return builder.Build(); } // specific configuration public access ====================================================================== // general telemetry public static bool GetUTCStamps() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryUTCTimestampsKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsTelemetryUTCTimestampsDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Telemetry?.TelemetryUseUtcTimeStamps ?? false; } - public static void SetUTCStamps(bool state) + public static void SetUTCStamps(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - _logger.Debug("Setting telemetry utc timestamps..."); - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryUTCTimestampsKey, state); + _logger.Debug("Setting telemetry utc timestamps to {@TelemetryStatus}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new TelemetrySection() {TelemetryStatus = state}); } // routes public static bool GetRoutesServerStatus() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsRoutesSection, PyRevitConsts.ConfigsRoutesServerKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsRoutesServerDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Routes.Status ?? false; } - public static void SetRoutesServerStatus(bool state) + public static void SetRoutesServerStatus(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsRoutesSection, PyRevitConsts.ConfigsRoutesServerKey, state); + _logger.Debug("Setting routes server status to {@Status}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new RoutesSection() {Status = state}); } - public static void EnableRoutesServer() => SetRoutesServerStatus(true); + public static void EnableRoutesServer(string revitVersion = ConfigurationService.DefaultConfigurationName) + => SetRoutesServerStatus(true, revitVersion); - public static void DisableRoutesServer() => SetRoutesServerStatus(false); + public static void DisableRoutesServer(string revitVersion = ConfigurationService.DefaultConfigurationName) + => SetRoutesServerStatus(false, revitVersion); public static string GetRoutesServerHost() { - var cfg = GetConfigFile(); - return cfg.GetValue(PyRevitConsts.ConfigsRoutesSection, PyRevitConsts.ConfigsRoutesHostKey); + IConfigurationService cfg = GetConfigFile(); + return cfg.Routes.Host; } - public static void SetRoutesServerHost(string host) + public static void SetRoutesServerHost(string host, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsRoutesSection, PyRevitConsts.ConfigsRoutesHostKey, host); + _logger.Debug("Setting routes server host to {@Host}...", host); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new RoutesSection() {Host = host}); } public static int GetRoutesServerPort() { - var cfg = GetConfigFile(); - var port = cfg.GetValue(PyRevitConsts.ConfigsRoutesSection, PyRevitConsts.ConfigsRoutesPortKey); - return port != null ? int.Parse(port) : PyRevitConsts.ConfigsRoutesPortDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Routes.Port ?? 48884; } - public static void SetRoutesServerPort(int port) + public static void SetRoutesServerPort(int port, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsRoutesSection, PyRevitConsts.ConfigsRoutesPortKey, port); + _logger.Debug("Setting routes server port to {@Port}...", port); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new RoutesSection() {Port = port}); } public static bool GetRoutesLoadCoreAPIStatus() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsRoutesSection, PyRevitConsts.ConfigsLoadCoreAPIKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsRoutesServerDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Routes.LoadCoreApi ?? false; } - public static void SetRoutesLoadCoreAPIStatus(bool state) + public static void SetRoutesLoadCoreAPIStatus(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsRoutesSection, PyRevitConsts.ConfigsLoadCoreAPIKey, state); + _logger.Debug("Setting routes load core API status to {@LoadCoreApi}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new RoutesSection() {LoadCoreApi = state}); } // telemetry public static bool GetTelemetryStatus() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryStatusKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsTelemetryStatusDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Telemetry?.TelemetryStatus ?? false; } - public static void SetTelemetryStatus(bool state) + public static void SetTelemetryStatus(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryStatusKey, state); + _logger.Debug("Setting telemetry status to {@TelemetryStatus}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new TelemetrySection() {TelemetryStatus = state}); } public static string GetTelemetryFilePath() { - var cfg = GetConfigFile(); - return cfg.GetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryFileDirKey) ?? string.Empty; + IConfigurationService cfg = GetConfigFile(); + return cfg.Telemetry.TelemetryFileDir; } public static string GetTelemetryServerUrl() { - var cfg = GetConfigFile(); - return cfg.GetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryServerUrlKey) ?? string.Empty; + IConfigurationService cfg = GetConfigFile(); + return cfg.Telemetry.TelemetryServerUrl; } - public static void EnableTelemetry(string telemetryFileDir = null, string telemetryServerUrl = null) + public static void EnableTelemetry(string telemetryFileDir = null, + string telemetryServerUrl = null, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - _logger.Debug(string.Format("Enabling telemetry... path: \"{0}\" server: {1}", - telemetryFileDir, telemetryServerUrl)); - SetTelemetryStatus(true); + _logger.Debug("Enabling telemetry..."); - if (telemetryFileDir != null) + if (!Directory.Exists(telemetryFileDir)) { - if (telemetryFileDir == string.Empty) - { - // set empty value - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryFileDirKey, telemetryFileDir); - } - else - { - if (CommonUtils.VerifyPath(telemetryFileDir)) - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryFileDirKey, telemetryFileDir); - else - _logger.Debug("Invalid log path \"{0}\"", telemetryFileDir); - } + _logger.Warn("Directory \"{@TelemetryFileDir}\" does not exist", telemetryFileDir); + telemetryFileDir = default; } - if (telemetryServerUrl != null) - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryServerUrlKey, telemetryServerUrl); + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, + new TelemetrySection() + { + TelemetryStatus = true, + TelemetryFileDir = telemetryFileDir, + TelemetryServerUrl = telemetryServerUrl + }); } public static bool GetTelemetryIncludeHooks() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryIncludeHooksKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsTelemetryIncludeHooksDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Telemetry.TelemetryIncludeHooks ?? false; } - public static void SetTelemetryIncludeHooks(bool state) + public static void SetTelemetryIncludeHooks(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryIncludeHooksKey, state); + _logger.Debug("Setting telemetry include hooks to {@TelemetryIncludeHooks}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new TelemetrySection() {TelemetryIncludeHooks = state}); } - public static void DisableTelemetry() + public static void DisableTelemetry(string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); _logger.Debug("Disabling telemetry..."); - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsTelemetryStatusKey, false); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new TelemetrySection() {TelemetryStatus = false}); } // app telemetry public static bool GetAppTelemetryStatus() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsAppTelemetryStatusKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsAppTelemetryStatusDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Telemetry.AppTelemetryStatus ?? false; } - public static void SetAppTelemetryStatus(bool state) + public static void SetAppTelemetryStatus(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsAppTelemetryStatusKey, state); + _logger.Debug("Setting app telemetry status to {@AppTelemetryStatus}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new TelemetrySection() {AppTelemetryStatus = state}); } public static string GetAppTelemetryServerUrl() { - var cfg = GetConfigFile(); - return cfg.GetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsAppTelemetryServerUrlKey) ?? string.Empty; + IConfigurationService cfg = GetConfigFile(); + return cfg.Telemetry.AppTelemetryServerUrl; } - public static void EnableAppTelemetry(string apptelemetryServerUrl = null) + public static void EnableAppTelemetry(string apptelemetryServerUrl = null, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - _logger.Debug(string.Format("Enabling app telemetry... server: {0}", apptelemetryServerUrl)); - SetAppTelemetryStatus(true); + _logger.Debug("Enabling app telemetry..."); - if (apptelemetryServerUrl != null) - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsAppTelemetryServerUrlKey, apptelemetryServerUrl); + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new TelemetrySection() {AppTelemetryServerUrl = apptelemetryServerUrl}); } - public static void DisableAppTelemetry() + public static void DisableAppTelemetry(string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); _logger.Debug("Disabling app telemetry..."); - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsAppTelemetryStatusKey, false); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new TelemetrySection() {AppTelemetryStatus = false}); } public static string GetAppTelemetryFlags() { - var cfg = GetConfigFile(); - return cfg.GetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsAppTelemetryEventFlagsKey) ?? string.Empty; + IConfigurationService cfg = GetConfigFile(); + return cfg.Telemetry.AppTelemetryEventFlags?.ToString("X") ?? string.Empty; } - public static void SetAppTelemetryFlags(string flags) + public static void SetAppTelemetryFlags(string flags, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - _logger.Debug("Setting app telemetry flags..."); - if (flags != null) - cfg.SetValue(PyRevitConsts.ConfigsTelemetrySection, PyRevitConsts.ConfigsAppTelemetryEventFlagsKey, flags); + _logger.Debug("Setting app telemetry flags to {@AppTelemetryEventFlags}...", flags); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, + new TelemetrySection() + {AppTelemetryEventFlags = int.Parse(flags, System.Globalization.NumberStyles.HexNumber)}); } // caching public static bool GetBinaryCaches() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsBinaryCacheKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsBinaryCacheDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.BinCache ?? false; } - public static void SetBinaryCaches(bool state) + public static void SetBinaryCaches(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsBinaryCacheKey, state); + _logger.Debug("Setting binary caches {@BinCache}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {BinCache = state}); } // update checking config public static bool GetCheckUpdates() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCheckUpdatesKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsCheckUpdatesDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.CheckUpdates ?? false; } - public static void SetCheckUpdates(bool state) + public static void SetCheckUpdates(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCheckUpdatesKey, state); + _logger.Debug("Setting check updates to {@CheckUpdates}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {CheckUpdates = state}); } // auto update config public static bool GetAutoUpdate() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsAutoUpdateKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsAutoUpdateDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.AutoUpdate ?? false; } - public static void SetAutoUpdate(bool state) + public static void SetAutoUpdate(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsAutoUpdateKey, state); + _logger.Debug("Setting auto update to {@AutoUpdate}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {AutoUpdate = state}); } // rocket mode config public static bool GetRocketMode() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsRocketModeKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsRocketModeDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.RocketMode ?? false; } - public static void SetRocketMode(bool state) + public static void SetRocketMode(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsRocketModeKey, state); + _logger.Debug("Setting rocket mode to {@RocketMode}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {RocketMode = state}); } // logging level config public static PyRevitLogLevels GetLoggingLevel() { - var cfg = GetConfigFile(); - var verboseCfg = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsVerboseKey); - bool verbose = verboseCfg != null ? bool.Parse(verboseCfg) : PyRevitConsts.ConfigsVerboseDefault; - - var debugCfg = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsDebugKey); - bool debug = debugCfg != null ? bool.Parse(debugCfg) : PyRevitConsts.ConfigsDebugDefault; + IConfigurationService cfg = GetConfigFile(); - if (verbose && !debug) + if (cfg.Core.Verbose == true + && cfg.Core.Debug != true) return PyRevitLogLevels.Verbose; - else if (debug) + else if (cfg.Core.Debug == true) return PyRevitLogLevels.Debug; return PyRevitLogLevels.Quiet; } - public static void SetLoggingLevel(PyRevitLogLevels level) + public static void SetLoggingLevel(PyRevitLogLevels level, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); + _logger.Debug("Setting logging level to {@LogLevel}...", level); + + IConfigurationService cfg = GetConfigFile(); if (level == PyRevitLogLevels.Quiet) { - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsVerboseKey, false); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsDebugKey, false); + cfg.SaveSection(revitVersion, new CoreSection() {Debug = false, Verbose = false}); } - - if (level == PyRevitLogLevels.Verbose) + else if (level == PyRevitLogLevels.Verbose) { - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsVerboseKey, true); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsDebugKey, false); + cfg.SaveSection(revitVersion, new CoreSection() {Debug = false, Verbose = true}); } - - if (level == PyRevitLogLevels.Debug) + else if (level == PyRevitLogLevels.Debug) { - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsVerboseKey, true); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsDebugKey, true); + cfg.SaveSection(revitVersion, new CoreSection() {Debug = true, Verbose = false}); } } // file logging config public static bool GetFileLogging() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsFileLoggingKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsFileLoggingDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.FileLogging ?? false; } - public static void SetFileLogging(bool state) + public static void SetFileLogging(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsFileLoggingKey, state); + _logger.Debug("Setting file logging to {@FileLogging}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {FileLogging = state}); } // misc startup public static int GetStartupLogTimeout() { - var cfg = GetConfigFile(); - var timeout = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsStartupLogTimeoutKey); - return timeout != null ? int.Parse(timeout) : PyRevitConsts.ConfigsStartupLogTimeoutDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.StartupLogTimeout ?? 0; } - public static void SetStartupLogTimeout(int timeout) + public static void SetStartupLogTimeout(int timeout, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsStartupLogTimeoutKey, timeout); + _logger.Debug("Setting startup log timeout to {@StartupLogTimeout}...", timeout); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {StartupLogTimeout = timeout}); } public static string GetRequiredHostBuild() { - var cfg = GetConfigFile(); - var timeout = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsRequiredHostBuildKey); - return timeout != null ? timeout : string.Empty; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.RequiredHostBuild ?? string.Empty; } - public static void SetRequiredHostBuild(string buildnumber) + public static void SetRequiredHostBuild(string buildnumber, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsRequiredHostBuildKey, buildnumber); + _logger.Debug("Setting required host build to {@RequiredHostBuild}...", buildnumber); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {RequiredHostBuild = buildnumber}); } - public static int GetMinHostDriveFreeSpace() + public static long GetMinHostDriveFreeSpace() { - var cfg = GetConfigFile(); - var timeout = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsMinDriveSpaceKey); - return timeout != null ? int.Parse(timeout) : 0; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.MinHostDriveFreeSpace ?? 0; } - public static void SetMinHostDriveFreeSpace(int freespace) + public static void SetMinHostDriveFreeSpace(long freespace, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsMinDriveSpaceKey, freespace); + _logger.Debug("Setting min host drive free space to {@MinHostDriveFreeSpace}...", freespace); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {MinHostDriveFreeSpace = freespace}); } // load beta config public static bool GetLoadBetaTools() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsLoadBetaKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsLoadBetaDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.LoadBeta ?? false; } - public static void SetLoadBetaTools(bool state) + public static void SetLoadBetaTools(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsLoadBetaKey, state); + _logger.Debug("Setting load beta tools to {@LoadBeta}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {LoadBeta = state}); } // cpythonengine public static int GetCpythonEngineVersion() { - var cfg = GetConfigFile(); - var timeout = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCPythonEngineKey); - return timeout != null ? int.Parse(timeout) : PyRevitConsts.ConfigsCPythonEngineDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.CpythonEngineVersion ?? 0; } - public static void SetCpythonEngineVersion(int version) + public static void SetCpythonEngineVersion(int version, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCPythonEngineKey, version); + _logger.Debug("Setting cpyhon engine version to {@CpythonEngineVersion}...", version); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {CpythonEngineVersion = version}); } // ux ui public static string GetUserLocale() { - var cfg = GetConfigFile(); - return cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsLocaleKey) ?? string.Empty; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.UserLocale ?? "en_us"; } - public static void SetUserLocale(string localCode) + public static void SetUserLocale(string localCode, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsLocaleKey, localCode); + _logger.Debug("Setting user locale to {@LocalCode}...", localCode); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {UserLocale = localCode}); } public static string GetOutputStyleSheet() { - var cfg = GetConfigFile(); - return cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsOutputStyleSheet) ?? string.Empty; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.OutputStyleSheet ?? string.Empty; } - public static void SetOutputStyleSheet(string outputCSSFilePath) + public static void SetOutputStyleSheet(string outputCssFilePath, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - if (File.Exists(outputCSSFilePath)) - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsOutputStyleSheet, outputCSSFilePath); + _logger.Debug("Setting output style sheet to {@OutputCssFilePath}...", outputCssFilePath); + + IConfigurationService cfg = GetConfigFile(); + if (File.Exists(outputCssFilePath)) + cfg.SaveSection(revitVersion, new CoreSection() {OutputStyleSheet = outputCssFilePath}); } // user access to tools public static bool GetUserCanUpdate() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsUserCanUpdateKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsUserCanUpdateDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.UserCanUpdate ?? false; } public static bool GetUserCanExtend() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsUserCanExtendKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsUserCanExtendDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.UserCanExtend ?? false; } public static bool GetUserCanConfig() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsUserCanConfigKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsUserCanConfigDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.UserCanConfig ?? false; } - public static void SetUserCanUpdate(bool state) + public static void SetUserCanUpdate(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsUserCanUpdateKey, state); + _logger.Debug("Setting user can install to {@UserCanUpdate}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {UserCanUpdate = state}); } - public static void SetUserCanExtend(bool state) + public static void SetUserCanExtend(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsUserCanExtendKey, state); + _logger.Debug("Setting user can install to {@UserCanExtend}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {UserCanExtend = state}); } - public static void SetUserCanConfig(bool state) + public static void SetUserCanConfig(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsUserCanConfigKey, state); + _logger.Debug("Setting user can install to {@UserCanConfig}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {UserCanConfig = state}); } public static bool GetColorizeDocs() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsColorizeDocsKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsColorizeDocsDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.ColorizeDocs ?? false; } - public static void SetColorizeDocs(bool state) + public static void SetColorizeDocs(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsColorizeDocsKey, state); + _logger.Debug("Setting colorize docs to {@ColorizeDocs}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {ColorizeDocs = state}); } public static bool GetAppendTooltipEx() { - var cfg = GetConfigFile(); - var status = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsAppendTooltipExKey); - return status != null ? bool.Parse(status) : PyRevitConsts.ConfigsAppendTooltipExDefault; + IConfigurationService cfg = GetConfigFile(); + return cfg.Core.TooltipDebugInfo ?? false; } - public static void SetAppendTooltipEx(bool state) + public static void SetAppendTooltipEx(bool state, + string revitVersion = ConfigurationService.DefaultConfigurationName) { - var cfg = GetConfigFile(); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsAppendTooltipExKey, state); + _logger.Debug("Setting tooltip debug info to {@TooltipDebugInfo}...", state); + + IConfigurationService cfg = GetConfigFile(); + cfg.SaveSection(revitVersion, new CoreSection() {TooltipDebugInfo = state}); } } } From 1b923807ecf7386b21ea3431134349965f11709c Mon Sep 17 00:00:00 2001 From: dosymep Date: Fri, 27 Dec 2024 15:18:22 +0300 Subject: [PATCH 22/37] update read and write propeties --- .../ConfigurationService.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index 6a315192f..2661f3261 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -115,8 +115,8 @@ private static void SaveSection(Type configurationType, object? sectionValue, IC { string sectionName = GetCustomAttribute(configurationType)?.SectionName ?? configurationType.Name; - - foreach (var propertyInfo in configurationType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + + foreach (var propertyInfo in GetProperties(configurationType)) { string keyName = GetCustomAttribute(propertyInfo)?.KeyName ?? propertyInfo.Name; object? keyValue = propertyInfo.GetValue(sectionValue); @@ -132,7 +132,7 @@ private static object CreateSection(Type configurationType, params IConfiguratio var sectionConfiguration = Activator.CreateInstance(configurationType); - foreach (var propertyInfo in configurationType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + foreach (var propertyInfo in GetProperties(configurationType)) { string keyName = GetCustomAttribute(propertyInfo)?.KeyName ?? propertyInfo.Name; @@ -152,6 +152,13 @@ private static object CreateSection(Type configurationType, params IConfiguratio .Select(item=> item.GetValueOrDefault(propertyInfo.PropertyType, sectionName, keyName)) .FirstOrDefault(item => item != default); } + + private static IEnumerable GetProperties(Type configurationType) + { + var flags = BindingFlags.Instance | BindingFlags.Public; + return configurationType.GetProperties(flags) + .Where(item => item.CanWrite && item.CanRead); + } private static T? GetCustomAttribute(MemberInfo memberInfo) where T : Attribute { From 7faee38a0520b771f18549159b632fec0e13691e Mon Sep 17 00:00:00 2001 From: dosymep Date: Fri, 27 Dec 2024 16:49:27 +0300 Subject: [PATCH 23/37] update pyRevit environment config --- dev/pyRevitLabs/pyRevitCLI/PyRevitCLI.cs | 11 +- .../pyRevitCLI/PyRevitCLIExtensionCmds.cs | 10 +- .../Abstractions/IConfigurationService.cs | 1 + .../ConfigurationService.cs | 2 + .../EnvironmentSection.cs | 13 ++ .../pyRevitLabs.PyRevit/PyRevitClones.cs | 47 +++--- .../pyRevitLabs.PyRevit/PyRevitExtensions.cs | 145 ++++++++++-------- 7 files changed, 136 insertions(+), 93 deletions(-) create mode 100644 dev/pyRevitLabs/pyRevitLabs.Configurations/EnvironmentSection.cs diff --git a/dev/pyRevitLabs/pyRevitCLI/PyRevitCLI.cs b/dev/pyRevitLabs/pyRevitCLI/PyRevitCLI.cs index 9f1d52761..0caeaa892 100644 --- a/dev/pyRevitLabs/pyRevitCLI/PyRevitCLI.cs +++ b/dev/pyRevitLabs/pyRevitCLI/PyRevitCLI.cs @@ -6,11 +6,13 @@ using DocoptNet; using pyRevitCLI.Properties; using pyRevitLabs.Common; +using pyRevitLabs.Configurations; using pyRevitLabs.NLog; using pyRevitLabs.NLog.Config; using pyRevitLabs.NLog.Targets; using pyRevitLabs.PyRevit; using Console = Colorful.Console; +using Environment = System.Environment; // NOTE: @@ -440,12 +442,15 @@ private static void ProcessArguments() { } else if (any("enable", "disable")) + { + string revitVersion = TryGetValue("") ?? ConfigurationService.DefaultConfigurationName; PyRevitCLIExtensionCmds.ToggleExtension( + revitVersion: revitVersion, enable: arguments["enable"].IsTrue, cloneName: TryGetValue(""), extName: TryGetValue("") - ); - + ); + } else if (all("sources")) { if (IsHelpMode) PyRevitCLIAppHelps.PrintHelp(PyRevitCLICommandType.ExtensionsSources); @@ -599,7 +604,7 @@ private static void ProcessArguments() { } else if (all("configs")) { - string revitVersion = TryGetValue(""); + string revitVersion = TryGetValue("") ?? ConfigurationService.DefaultConfigurationName; if (IsHelpMode) PyRevitCLIAppHelps.PrintHelp(PyRevitCLICommandType.Configs); diff --git a/dev/pyRevitLabs/pyRevitCLI/PyRevitCLIExtensionCmds.cs b/dev/pyRevitLabs/pyRevitCLI/PyRevitCLIExtensionCmds.cs index 61cc5bb96..f0cc3de57 100644 --- a/dev/pyRevitLabs/pyRevitCLI/PyRevitCLIExtensionCmds.cs +++ b/dev/pyRevitLabs/pyRevitCLI/PyRevitCLIExtensionCmds.cs @@ -171,7 +171,7 @@ internal static void } internal static void - ToggleExtension(bool enable, string cloneName, string extName) { + ToggleExtension(string revitVersion, bool enable, string cloneName, string extName) { if (extName != null) { PyRevitClone clone = null; if (cloneName != null) @@ -179,15 +179,15 @@ internal static void if (enable) { if (clone != null) - PyRevitExtensions.EnableShippedExtension(clone, extName); + PyRevitExtensions.EnableShippedExtension(revitVersion, clone, extName); else - PyRevitExtensions.EnableInstalledExtension(extName); + PyRevitExtensions.EnableInstalledExtension(revitVersion, extName); } else { if (clone != null) - PyRevitExtensions.DisableShippedExtension(clone, extName); + PyRevitExtensions.DisableShippedExtension(revitVersion, clone, extName); else - PyRevitExtensions.DisableInstalledExtension(extName); + PyRevitExtensions.DisableInstalledExtension(revitVersion, extName); } } } diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs index f9434521e..8064d162d 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs @@ -5,6 +5,7 @@ public interface IConfigurationService CoreSection Core { get; } RoutesSection Routes { get; } TelemetrySection Telemetry { get; } + EnvironmentSection Environment { get; } T GetSection(); void SaveSection(string configurationName, T sectionValue); diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index 2661f3261..2324a76e6 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -41,12 +41,14 @@ internal static IConfigurationService Create( public CoreSection Core { get; private set; } = new(); public RoutesSection Routes { get; private set; } = new(); public TelemetrySection Telemetry { get; private set; } = new(); + public EnvironmentSection Environment { get; private set; } = new(); private void LoadConfigurations() { Core = GetSection(); Routes = GetSection(); Telemetry = GetSection(); + Environment = GetSection(); } public T GetSection() diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/EnvironmentSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/EnvironmentSection.cs new file mode 100644 index 000000000..d9c84a777 --- /dev/null +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/EnvironmentSection.cs @@ -0,0 +1,13 @@ +using pyRevitLabs.Configurations.Attributes; + +namespace pyRevitLabs.Configurations; + +[SectionName("environment")] +public record EnvironmentSection +{ + [KeyName("sources")] + public List Sources { get; set; } = new(); + + [KeyName("clones")] + public Dictionary Clones { get; set; } = new(); +} \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitClones.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitClones.cs index b317233a1..649d61b0b 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitClones.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitClones.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Linq; using System.Security.Principal; using System.Text; using System.Text.RegularExpressions; using pyRevitLabs.Common; using pyRevitLabs.Common.Extensions; +using pyRevitLabs.Configurations; using pyRevitLabs.Json.Linq; using pyRevitLabs.NLog; @@ -98,30 +100,27 @@ public static List GetRegisteredClones() { // safely get clone list var cfg = PyRevitConfigs.GetConfigFile(); - var clonesList = cfg.GetDictValue(PyRevitConsts.EnvConfigsSectionName, PyRevitConsts.EnvConfigsInstalledClonesKey); - - if (clonesList != null) { - // verify all registered clones, protect against tampering - foreach (var cloneKeyValue in clonesList) { - var clonePath = cloneKeyValue.Value.NormalizeAsPath(); - if (CommonUtils.VerifyPath(clonePath)) { - try { - var clone = new PyRevitClone(clonePath, name: cloneKeyValue.Key); - if (clone.IsValid && !validatedClones.Contains(clone)) { - logger.Debug("Verified clone \"{0}={1}\"", clone.Name, clone.ClonePath); - validatedClones.Add(clone); - } - } - catch { - logger.Debug("Error occured when processing registered clone \"{0}\" at \"{1}\"", cloneKeyValue.Key, clonePath); + + // verify all registered clones, protect against tampering + foreach (var cloneKeyValue in cfg.Environment.Clones) { + var clonePath = cloneKeyValue.Value.NormalizeAsPath(); + if (CommonUtils.VerifyPath(clonePath)) { + try { + var clone = new PyRevitClone(clonePath, name: cloneKeyValue.Key); + if (clone.IsValid && !validatedClones.Contains(clone)) { + logger.Debug("Verified clone \"{@CloneName}={@ClonePath}\"", clone.Name, clone.ClonePath); + validatedClones.Add(clone); } } + catch { + logger.Debug("Error occured when processing registered clone \"{@CloneName}\" at \"{@ClonePath}\"", cloneKeyValue.Key, clonePath); + } } - - // rewrite the verified clones list back to config file - SaveRegisteredClones(validatedClones); } + // rewrite the verified clones list back to config file + SaveRegisteredClones(validatedClones); + return validatedClones; } @@ -551,12 +550,10 @@ public static void UpdateAllClones(GitInstallerCredentials credentials) { // updates the config value for registered clones public static void SaveRegisteredClones(IEnumerable clonesList) { var cfg = PyRevitConfigs.GetConfigFile(); - var newValueDic = new Dictionary(); - foreach (var clone in clonesList) - newValueDic[clone.Name] = clone.ClonePath; - - cfg.SetValue(PyRevitConsts.EnvConfigsSectionName, PyRevitConsts.EnvConfigsInstalledClonesKey, newValueDic); + cfg.SaveSection( + ConfigurationService.DefaultConfigurationName, + new EnvironmentSection() + {Clones = clonesList.ToDictionary(item => item.Name, item => item.ClonePath)}); } - } } diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs index a8ad2ba87..abb625985 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs @@ -2,14 +2,18 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Linq; using System.Security.Principal; using System.Text; using System.Text.RegularExpressions; using pyRevitLabs.Common; using pyRevitLabs.Common.Extensions; +using pyRevitLabs.Configurations; +using pyRevitLabs.Configurations.Abstractions; using pyRevitLabs.Json.Linq; using pyRevitLabs.NLog; +using Environment = System.Environment; /* * There are 3 types of extension functions here @@ -20,7 +24,7 @@ namespace pyRevitLabs.PyRevit { public static class PyRevitExtensions { - private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); // managing extensions ======================================================================================= // check if extension name matches the given pattern @@ -34,10 +38,10 @@ private static bool CompareExtensionNames(string extName, string searchTerm) { public static List FindExtensions(string searchPath) { var installedExtensions = new List(); - logger.Debug("Looking for installed extensions under \"{0}\"...", searchPath); + _logger.Debug("Looking for installed extensions under \"{0}\"...", searchPath); foreach (var subdir in Directory.GetDirectories(searchPath)) { if (PyRevitExtension.IsExtensionDirectory(subdir)) { - logger.Debug("Found installed extension \"{0}\"...", subdir); + _logger.Debug("Found installed extension \"{0}\"...", subdir); installedExtensions.Add(new PyRevitExtension(subdir)); } } @@ -66,7 +70,7 @@ public static List LookupRegisteredExtensions(string searchPattern); } catch (Exception ex) { - logger.Error( + _logger.Error( string.Format( "Error looking up extension with pattern \"{0}\" in default extension source." + " | {1}", searchPattern, ex.Message) @@ -83,7 +87,7 @@ public static List LookupRegisteredExtensions(string return matchedExtensions; } catch (Exception ex) { - logger.Error( + _logger.Error( string.Format( "Error looking up extension with pattern \"{0}\" in extension lookup source \"{1}\"" + " | {2}", searchPattern, extLookupSrc, ex.Message) @@ -98,13 +102,13 @@ public static List LookupRegisteredExtensions(string // lookup registered extension by name // @handled @logs public static PyRevitExtensionDefinition FindRegisteredExtension(string extensionName) { - logger.Debug("Looking up registered extension \"{0}\"...", extensionName); + _logger.Debug("Looking up registered extension \"{0}\"...", extensionName); var matchingExts = LookupRegisteredExtensions(extensionName); if (matchingExts.Count == 0) { throw new PyRevitException(string.Format("Can not find extension \"{0}\"", extensionName)); } else if (matchingExts.Count == 1) { - logger.Debug("Extension found \"{0}\"...", matchingExts[0].Name); + _logger.Debug("Extension found \"{0}\"...", matchingExts[0].Name); return matchingExts[0]; } else if (matchingExts.Count > 1) @@ -132,11 +136,11 @@ public static List GetInstalledExtensions(string searchPath = // find extension installed under registered search paths // @handled @logs public static PyRevitExtension GetInstalledExtension(string searchPattern) { - logger.Debug("Looking up installed extension \"{0}\"...", searchPattern); + _logger.Debug("Looking up installed extension \"{0}\"...", searchPattern); foreach (var ext in GetInstalledExtensions()) { - logger.Debug("-----------> {0}", ext.Name); + _logger.Debug("-----------> {0}", ext.Name); if (CompareExtensionNames(ext.Name, searchPattern)) { - logger.Debug(string.Format("\"{0}\" Matched installed extension \"{1}\"", + _logger.Debug(string.Format("\"{0}\" Matched installed extension \"{1}\"", searchPattern, ext.Name)); return ext; } @@ -181,8 +185,8 @@ public static void InstallExtension(string extensionName, PyRevitExtensionTypes // determine branch name branchName = branchName ?? PyRevitConsts.DefaultExtensionRepoDefaultBranch; - logger.Debug("Extension branch name determined as \"{0}\"", branchName); - logger.Debug("Installing extension into \"{0}\"", finalExtRepoPath); + _logger.Debug("Extension branch name determined as \"{0}\"", branchName); + _logger.Debug("Installing extension into \"{0}\"", finalExtRepoPath); // start the clone process var repo = GitInstaller.Clone(repoPath, branchName, finalExtRepoPath, credentials); @@ -192,16 +196,16 @@ public static void InstallExtension(string extensionName, PyRevitExtensionTypes // make sure to delete the repo if error occured after cloning var clonedPath = repo.Info.WorkingDirectory; if (GitInstaller.IsValidRepo(clonedPath)) { - logger.Debug("Clone successful \"{0}\"", clonedPath); + _logger.Debug("Clone successful \"{0}\"", clonedPath); RegisterExtensionSearchPath(destPath.NormalizeAsPath()); } else { - logger.Debug("Invalid repo after cloning. Deleting clone \"{0}\"", repoPath); + _logger.Debug("Invalid repo after cloning. Deleting clone \"{0}\"", repoPath); try { CommonUtils.DeleteDirectory(repoPath); } catch (Exception delEx) { - logger.Error(string.Format("Error post-install cleanup on \"{0}\" | {1}", + _logger.Error(string.Format("Error post-install cleanup on \"{0}\" | {1}", repoPath, delEx.Message)); } } @@ -216,7 +220,7 @@ public static void InstallExtension(string extensionName, PyRevitExtensionTypes // @handled @logs public static void InstallExtension(PyRevitExtensionDefinition extDef, string destPath = null, string branchName = null) { - logger.Debug("Installing extension \"{0}\"", extDef.Name); + _logger.Debug("Installing extension \"{0}\"", extDef.Name); InstallExtension(extDef.Name, extDef.Type, extDef.Url, destPath, branchName); } @@ -224,7 +228,7 @@ public static void InstallExtension(PyRevitExtensionDefinition extDef, // @handled @logs public static void RemoveExtension(string repoPath, bool removeSearchPath = false) { if (repoPath != null) { - logger.Debug("Uninstalling extension at \"{0}\"", repoPath); + _logger.Debug("Uninstalling extension at \"{0}\"", repoPath); CommonUtils.DeleteDirectory(repoPath); // remove search path if requested if (removeSearchPath) @@ -243,7 +247,7 @@ public static void UninstallExtension(PyRevitExtension ext, bool removeSearchPat // uninstalls an extension by name // @handled @logs public static void UninstallExtension(string extensionName, bool removeSearchPath = false) { - logger.Debug("Uninstalling extension \"{0}\"", extensionName); + _logger.Debug("Uninstalling extension \"{0}\"", extensionName); var ext = GetInstalledExtension(extensionName); RemoveExtension(ext.InstallPath, removeSearchPath: removeSearchPath); } @@ -251,8 +255,8 @@ public static void UninstallExtension(string extensionName, bool removeSearchPat // force update extension // @handled @logs public static void UpdateExtension(PyRevitExtension ext, GitInstallerCredentials credentials = null) { - logger.Debug("Updating extension \"{0}\"", ext.Name); - logger.Debug("Updating extension repo at \"{0}\"", ext.InstallPath); + _logger.Debug("Updating extension \"{0}\"", ext.Name); + _logger.Debug("Updating extension repo at \"{0}\"", ext.InstallPath); var res = GitInstaller.ForcedUpdate(ext.InstallPath, credentials); if (res <= UpdateStatus.Conflicts) throw new PyRevitException( @@ -268,7 +272,7 @@ public static void UpdateExtension(string extName, GitInstallerCredentials crede // force update all extensions // @handled @logs public static void UpdateAllInstalledExtensions(GitInstallerCredentials credentials = null) { - logger.Debug("Updating all installed extensions."); + _logger.Debug("Updating all installed extensions."); // update all installed extensions foreach (var ext in GetInstalledExtensions()) UpdateExtension(ext, credentials); @@ -276,58 +280,69 @@ public static void UpdateAllInstalledExtensions(GitInstallerCredentials credenti // enable extension in config // @handled @logs - private static void ToggleExtension(PyRevitExtension ext, bool state) { - logger.Debug("{0} extension \"{1}\"", state ? "Enabling" : "Disabling", ext.Name); - var cfg = PyRevitConfigs.GetConfigFile(); - cfg.SetValue(ext.ConfigName, PyRevitConsts.ExtensionDisabledKey, !state); + private static void ToggleExtension(string revitVersion, PyRevitExtension ext, bool state) { + _logger.Debug("{@State} extension \"{@ExtensionName}\"", state ? "Enabling" : "Disabling", ext.Name); + + IConfigurationService cfg = PyRevitConfigs.GetConfigFile(); + cfg.SetSectionKeyValue(revitVersion, ext.ConfigName, PyRevitConsts.ExtensionDisabledKey, !state); } // disable installed extension in config // @handled @logs - public static void EnableInstalledExtension(string searchPattern) { + public static void EnableInstalledExtension(string revitVersion, string searchPattern) { var ext = GetInstalledExtension(searchPattern); - ToggleExtension(ext, true); + ToggleExtension(revitVersion, ext, true); } // disable installed extension in config // @handled @logs - public static void DisableInstalledExtension(string searchPattern) { + public static void DisableInstalledExtension(string revitVersion, string searchPattern) { var ext = GetInstalledExtension(searchPattern); - ToggleExtension(ext, false); + ToggleExtension(revitVersion, ext, false); } // disable shipped extension in config // @handled @logs - public static void EnableShippedExtension(PyRevitClone clone, string searchPattern) { + public static void EnableShippedExtension(string revitVersion, PyRevitClone clone, string searchPattern) { var ext = GetShippedExtension(clone, searchPattern); - ToggleExtension(ext, true); + ToggleExtension(revitVersion, ext, true); } // disable shipped extension in config // @handled @logs - public static void DisableShippedExtension(PyRevitClone clone, string searchPattern) { + public static void DisableShippedExtension(string revitVersion, PyRevitClone clone, string searchPattern) { var ext = GetShippedExtension(clone, searchPattern); - ToggleExtension(ext, false); + ToggleExtension(revitVersion, ext, false); } // get list of registered extension search paths // @handled @logs public static List GetRegisteredExtensionSearchPaths() { + // TODO: Make apply config to revit version var validatedPaths = new List(); var cfg = PyRevitConfigs.GetConfigFile(); - var searchPaths = cfg.GetListValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsUserExtensionsKey); + + var searchPaths = cfg.GetSectionKeyValueOrDefault( + ConfigurationService.DefaultConfigurationName, + PyRevitConsts.ConfigsCoreSection, + PyRevitConsts.ConfigsUserExtensionsKey); + if (searchPaths != null) { // make sure paths exist foreach (var path in searchPaths) { var normPath = path.NormalizeAsPath(); if (CommonUtils.VerifyPath(path) && !validatedPaths.Contains(normPath)) { - logger.Debug("Verified extension search path \"{0}\"", normPath); + _logger.Debug("Verified extension search path \"{@ExtensionsSource}\"", normPath); validatedPaths.Add(normPath); } } // rewrite verified list - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsUserExtensionsKey, validatedPaths); + cfg.SetSectionKeyValue( + ConfigurationService.DefaultConfigurationName, + PyRevitConsts.ConfigsCoreSection, + PyRevitConsts.ConfigsUserExtensionsKey, + validatedPaths); } return validatedPaths; } @@ -335,12 +350,17 @@ public static List GetRegisteredExtensionSearchPaths() { // add extension search path // @handled @logs public static void RegisterExtensionSearchPath(string searchPath) { + // TODO: Make apply config to revit version var cfg = PyRevitConfigs.GetConfigFile(); if (CommonUtils.VerifyPath(searchPath)) { - logger.Debug("Adding extension search path \"{0}\"", searchPath); + _logger.Debug("Adding extension search path \"{@ExtensionSource}\"", searchPath); var searchPaths = GetRegisteredExtensionSearchPaths(); searchPaths.Add(searchPath.NormalizeAsPath()); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsUserExtensionsKey, searchPaths); + cfg.SetSectionKeyValue( + ConfigurationService.DefaultConfigurationName, + PyRevitConsts.ConfigsCoreSection, + PyRevitConsts.ConfigsUserExtensionsKey, + searchPaths); } else throw new pyRevitResourceMissingException(searchPath); @@ -351,10 +371,14 @@ public static void RegisterExtensionSearchPath(string searchPath) { public static void UnregisterExtensionSearchPath(string searchPath) { var cfg = PyRevitConfigs.GetConfigFile(); var normPath = searchPath.NormalizeAsPath(); - logger.Debug("Removing extension search path \"{0}\"", normPath); + _logger.Debug("Removing extension search path \"{@ExtensionSource}\"", normPath); var searchPaths = GetRegisteredExtensionSearchPaths(); searchPaths.Remove(normPath); - cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsUserExtensionsKey, searchPaths); + cfg.SetSectionKeyValue( + ConfigurationService.DefaultConfigurationName, + PyRevitConsts.ConfigsCoreSection, + PyRevitConsts.ConfigsUserExtensionsKey, + searchPaths); } // managing extension sources ================================================================================ @@ -368,16 +392,15 @@ public static string GetDefaultExtensionLookupSource() { // @handled @logs public static List GetRegisteredExtensionLookupSources() { var cfg = PyRevitConfigs.GetConfigFile(); + var normSources = new List(); - var sources = cfg.GetListValue(PyRevitConsts.EnvConfigsSectionName, PyRevitConsts.EnvConfigsExtensionLookupSourcesKey); - if (sources != null) { - foreach (var src in sources) { - var normSrc = src.NormalizeAsPath(); - logger.Debug("Extension lookup source \"{0}\"", normSrc); - normSources.Add(normSrc); - SaveExtensionLookupSources(normSources); - } + foreach (var src in cfg.Environment.Sources) { + var normSrc = src.NormalizeAsPath(); + _logger.Debug("Extension lookup source \"{@ExtensionSource}\"", normSrc); + normSources.Add(normSrc); + SaveExtensionLookupSources(normSources); } + return normSources; } @@ -387,12 +410,12 @@ public static void RegisterExtensionLookupSource(string extLookupSource) { var normSource = extLookupSource.NormalizeAsPath(); var sources = GetRegisteredExtensionLookupSources(); if (!sources.Contains(normSource)) { - logger.Debug("Registering extension lookup source \"{0}\"", normSource); + _logger.Debug("Registering extension lookup source \"{0}\"", normSource); sources.Add(normSource); SaveExtensionLookupSources(sources); } else - logger.Debug("Extension lookup source already exists. Skipping registration."); + _logger.Debug("Extension lookup source already exists. Skipping registration."); } // unregister extension lookup source @@ -401,12 +424,12 @@ public static void UnregisterExtensionLookupSource(string extLookupSource) { var normSource = extLookupSource.NormalizeAsPath(); var sources = GetRegisteredExtensionLookupSources(); if (sources.Contains(normSource)) { - logger.Debug("Unregistering extension lookup source \"{0}\"", normSource); + _logger.Debug("Unregistering extension lookup source \"{0}\"", normSource); sources.Remove(normSource); SaveExtensionLookupSources(sources); } else - logger.Debug("Extension lookup source does not exist. Skipping unregistration."); + _logger.Debug("Extension lookup source does not exist. Skipping unregistration."); } // unregister all extension lookup sources @@ -427,20 +450,20 @@ private static List LookupExtensionInDefinitionFile( string filePath = null; // determine if path is file or uri - logger.Debug("Determining file or remote source \"{0}\"", fileOrUri); + _logger.Debug("Determining file or remote source \"{0}\"", fileOrUri); Uri uriResult; var validPath = Uri.TryCreate(fileOrUri, UriKind.Absolute, out uriResult); if (validPath) { if (uriResult.IsFile) { filePath = fileOrUri; - logger.Debug("Source is a file \"{0}\"", filePath); + _logger.Debug("Source is a file \"{0}\"", filePath); } else if (uriResult.HostNameType == UriHostNameType.Dns || uriResult.HostNameType == UriHostNameType.IPv4 || uriResult.HostNameType == UriHostNameType.IPv6) { - logger.Debug("Source is a remote resource \"{0}\"", fileOrUri); - logger.Debug("Downloading remote resource \"{0}\"...", fileOrUri); + _logger.Debug("Source is a remote resource \"{0}\"", fileOrUri); + _logger.Debug("Downloading remote resource \"{0}\"...", fileOrUri); // download the resource into TEMP try { filePath = @@ -464,7 +487,7 @@ private static List LookupExtensionInDefinitionFile( // process file now if (filePath != null) { if (Path.GetExtension(filePath).ToLower() == ".json") { - logger.Debug("Parsing extension metadata file..."); + _logger.Debug("Parsing extension metadata file..."); dynamic extensionsObj; if (filePath != null) { @@ -479,10 +502,10 @@ private static List LookupExtensionInDefinitionFile( foreach (JObject extObj in extensionsObj.extensions) { var extDef = new PyRevitExtensionDefinition(extObj); - logger.Debug("Registered extension \"{0}\"", extDef.Name); + _logger.Debug("Registered extension \"{0}\"", extDef.Name); if (searchPattern != null) { if (CompareExtensionNames(extDef.Name, searchPattern)) { - logger.Debug(string.Format("\"{0}\" Matched registered extension \"{1}\"", + _logger.Debug(string.Format("\"{0}\" Matched registered extension \"{1}\"", searchPattern, extDef.Name)); pyrevtExts.Add(extDef); } @@ -505,7 +528,9 @@ private static List LookupExtensionInDefinitionFile( // save list of source exensio private static void SaveExtensionLookupSources(IEnumerable sources) { var cfg = PyRevitConfigs.GetConfigFile(); - cfg.SetValue(PyRevitConsts.EnvConfigsSectionName, PyRevitConsts.EnvConfigsExtensionLookupSourcesKey, sources); + cfg.SaveSection( + ConfigurationService.DefaultConfigurationName, + new EnvironmentSection() {Sources = sources.ToList()}); } } } From 3825ecb22cee86c22dea5cdd962f11cae27ede09 Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 09:06:36 +0300 Subject: [PATCH 24/37] fix typo --- dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs index e7e713265..8c72f9118 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs @@ -115,7 +115,7 @@ public static void SetupConfig(string templateConfigFilePath = null) } - _logger.Debug("Seeding config file \"{@SourceFile}\" to \"{@SargetFile}\"", sourceFile, targetFile); + _logger.Debug("Seeding config file \"{@SourceFile}\" to \"{@TargetFile}\"", sourceFile, targetFile); try { From 3b09e3e259edc0d55d52f16fa1d6d7e63f7db17b Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 09:13:01 +0300 Subject: [PATCH 25/37] fix override config name --- .../pyRevitLabs.PyRevit/PyRevitConfigs.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs index 8c72f9118..ac1bfa085 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs @@ -27,8 +27,7 @@ public static class PyRevitConfigs /// Returns config file. /// /// Returns admin config if admin config exists and user config not found. - public static IConfigurationService GetConfigFile( - string overrideName = ConfigurationService.DefaultConfigurationName) + public static IConfigurationService GetConfigFile(string overrideName = default) { // make sure the file exists and if not create an empty one string userConfig = PyRevitConsts.ConfigFilePath; @@ -127,15 +126,13 @@ public static void SetupConfig(string templateConfigFilePath = null) } } - private static IConfigurationService CreateConfiguration( - string configPath, - bool readOnly = false, - string overrideName = ConfigurationService.DefaultConfigurationName) + private static IConfigurationService CreateConfiguration(string configPath, bool readOnly, string overrideName) { var builder = new ConfigurationBuilder() - .AddIniConfiguration(configPath, "default", readOnly); + .AddIniConfiguration(configPath, ConfigurationService.DefaultConfigurationName, readOnly); - if (string.IsNullOrEmpty(overrideName)) + if (!string.IsNullOrEmpty(overrideName) + && overrideName?.Equals(ConfigurationService.DefaultConfigurationName) != true) { builder.AddIniConfiguration( Path.ChangeExtension(configPath, From 6f81e4a7be01cbe80db0995dccff5b7c152835db Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 09:57:45 +0300 Subject: [PATCH 26/37] update getting config file service --- .../pyRevitLabs.PyRevit/PyRevitConfigs.cs | 23 +++++++++++++++---- .../pyRevitLabs.PyRevit/PyRevitConsts.cs | 15 ++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs index ac1bfa085..e433fb1d0 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs @@ -26,21 +26,34 @@ public static class PyRevitConfigs /// /// Returns config file. /// - /// Returns admin config if admin config exists and user config not found. + /// Returns admin config if admin config exists and readonly and user config not found. + /// + /// Seeds admin config file when user config not found and admin config not readonly. + /// public static IConfigurationService GetConfigFile(string overrideName = default) { - // make sure the file exists and if not create an empty one string userConfig = PyRevitConsts.ConfigFilePath; string adminConfig = PyRevitConsts.AdminConfigFilePath; + // create admin config if (!File.Exists(userConfig) - && File.Exists(adminConfig)) + && File.Exists(adminConfig) + && new FileInfo(adminConfig).IsReadOnly) { - _logger.Info("Creating admin config {@ConfigPath}...", adminConfig); + _logger.Debug("Creating admin config service {@ConfigPath}...", adminConfig); return CreateConfiguration(adminConfig, true, overrideName); } - _logger.Info("Creating user config {@ConfigPath}...", userConfig); + // copy admin config to user config + // first run when user config not created + if (!File.Exists(userConfig) + && !new FileInfo(adminConfig).IsReadOnly) + { + _logger.Debug("Copy admin config file..."); + SetupConfig(adminConfig); + } + + _logger.Debug("Creating user config service {@ConfigPath}...", userConfig); return CreateConfiguration(userConfig, false, overrideName); } diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs index 7a9379536..1d1fd4f2a 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs @@ -198,21 +198,10 @@ public static string FindConfigFileInDirectory(string sourcePath) { // pyRevit config file path // @reviewed - public static string ConfigFilePath { - get { - string configRoot = UserEnv.IsRunAsElevated() ? PyRevitLabsConsts.PyRevitProgramDataPath : PyRevitLabsConsts.PyRevitPath; - var cfgFile = FindConfigFileInDirectory(configRoot); - return cfgFile != null ? cfgFile : Path.Combine(configRoot, DefaultConfigsFileName); - } - } + public static string ConfigFilePath => Path.Combine(PyRevitLabsConsts.PyRevitPath, DefaultConfigsFileName); // pyRevit config file path // @reviewed - public static string AdminConfigFilePath { - get { - var cfgFile = FindConfigFileInDirectory(PyRevitLabsConsts.PyRevitProgramDataPath); - return cfgFile != null ? cfgFile : Path.Combine(PyRevitLabsConsts.PyRevitProgramDataPath, DefaultConfigsFileName); - } - } + public static string AdminConfigFilePath => Path.Combine(PyRevitLabsConsts.PyRevitProgramDataPath, DefaultConfigsFileName); } } From 9e1f6bd9ebd3333d8951017504f6c82d04e3ac31 Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 10:04:08 +0300 Subject: [PATCH 27/37] add readonly properti to service (python) --- .../Abstractions/IConfigurationService.cs | 2 ++ .../ConfigurationBuilder.cs | 8 +++++++- .../ConfigurationService.cs | 11 +++++++---- dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs | 4 ++-- .../ConfigurationServiceFixture.cs | 3 +-- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs index 8064d162d..e637a1e04 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/Abstractions/IConfigurationService.cs @@ -2,6 +2,8 @@ public interface IConfigurationService { + bool ReadOnly { get; } + CoreSection Core { get; } RoutesSection Routes { get; } TelemetrySection Telemetry { get; } diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs index c7edf5e62..d70562baa 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBuilder.cs @@ -5,9 +5,15 @@ namespace pyRevitLabs.Configurations; public sealed class ConfigurationBuilder { + private readonly bool _readOnly; private readonly List _names = []; private readonly Dictionary _configurations = []; + public ConfigurationBuilder(bool readOnly) + { + _readOnly = readOnly; + } + public ConfigurationBuilder AddConfigurationSource(string configurationName, IConfiguration configuration) { if (configuration == null) @@ -24,6 +30,6 @@ public ConfigurationBuilder AddConfigurationSource(string configurationName, ICo public IConfigurationService Build() { - return ConfigurationService.Create(_names, _configurations); + return ConfigurationService.Create(_readOnly, _names, _configurations); } } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index 2324a76e6..4fe1db1fe 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -14,23 +14,26 @@ public sealed class ConfigurationService : IConfigurationService public const string DefaultConfigurationName = "Default"; - internal ConfigurationService( + internal ConfigurationService(bool readOnly, List names, IDictionary configurations) { _names = names; _configurations = configurations; + + ReadOnly = readOnly; // TODO: Change behavior LoadConfigurations(); } - internal static IConfigurationService Create( - List names, + internal static IConfigurationService Create(bool readOnly, List names, IDictionary configurations) { - return new ConfigurationService(names, configurations); + return new ConfigurationService(readOnly, names, configurations); } + + public bool ReadOnly { get; } public IEnumerable ConfigurationNames => _configurations.Keys; diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs index e433fb1d0..b76fa5a6e 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs @@ -141,7 +141,7 @@ public static void SetupConfig(string templateConfigFilePath = null) private static IConfigurationService CreateConfiguration(string configPath, bool readOnly, string overrideName) { - var builder = new ConfigurationBuilder() + var builder = new ConfigurationBuilder(readOnly) .AddIniConfiguration(configPath, ConfigurationService.DefaultConfigurationName, readOnly); if (!string.IsNullOrEmpty(overrideName) @@ -149,7 +149,7 @@ private static IConfigurationService CreateConfiguration(string configPath, bool { builder.AddIniConfiguration( Path.ChangeExtension(configPath, - $"{overrideName}.{IniConfiguration.DefaultFileExtension}"), overrideName); + $"{overrideName}.{IniConfiguration.DefaultFileExtension}"), overrideName, readOnly); } return builder.Build(); diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs index 5b46ba454..740c6cb98 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Tests/ConfigurationServiceFixture.cs @@ -7,8 +7,7 @@ public sealed class ConfigurationServiceFixture : IDisposable { public ConfigurationServiceFixture() { - Configuration = new ConfigurationService( - new List(), + Configuration = new ConfigurationService(false, new List(), new Dictionary() {{"default", new TestRunConfiguration()}}); } From 0b4fd1c1049986fa89d1630ef7288e76601cf4ca Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 10:37:58 +0300 Subject: [PATCH 28/37] add user extensions --- .../pyRevitLabs.Configurations/CoreSection.cs | 3 +++ .../pyRevitLabs.PyRevit/PyRevitExtensions.cs | 14 ++++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs index 496557c96..2d3c808f5 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/CoreSection.cs @@ -69,4 +69,7 @@ public sealed record CoreSection [KeyName("outputstylesheet")] public string? OutputStyleSheet { get; set; } + + [KeyName("userextensions")] + public List UserExtensions { get; set; } = new(); } \ No newline at end of file diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs index abb625985..8a4edf1fb 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs @@ -338,11 +338,8 @@ public static List GetRegisteredExtensionSearchPaths() { } // rewrite verified list - cfg.SetSectionKeyValue( - ConfigurationService.DefaultConfigurationName, - PyRevitConsts.ConfigsCoreSection, - PyRevitConsts.ConfigsUserExtensionsKey, - validatedPaths); + cfg.SaveSection( + ConfigurationService.DefaultConfigurationName, new CoreSection() {UserExtensions = validatedPaths}); } return validatedPaths; } @@ -356,11 +353,8 @@ public static void RegisterExtensionSearchPath(string searchPath) { _logger.Debug("Adding extension search path \"{@ExtensionSource}\"", searchPath); var searchPaths = GetRegisteredExtensionSearchPaths(); searchPaths.Add(searchPath.NormalizeAsPath()); - cfg.SetSectionKeyValue( - ConfigurationService.DefaultConfigurationName, - PyRevitConsts.ConfigsCoreSection, - PyRevitConsts.ConfigsUserExtensionsKey, - searchPaths); + cfg.SaveSection( + ConfigurationService.DefaultConfigurationName, new CoreSection() {UserExtensions = searchPaths}); } else throw new pyRevitResourceMissingException(searchPath); From 24927a5c202003397c453670c9f5330390c1473f Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 10:42:06 +0300 Subject: [PATCH 29/37] update python code --- pyrevitlib/pyrevit/loader/sessioninfo.py | 2 - pyrevitlib/pyrevit/userconfig.py | 473 +++++------------------ 2 files changed, 91 insertions(+), 384 deletions(-) diff --git a/pyrevitlib/pyrevit/loader/sessioninfo.py b/pyrevitlib/pyrevit/loader/sessioninfo.py index e68a30bef..0e62115ae 100644 --- a/pyrevitlib/pyrevit/loader/sessioninfo.py +++ b/pyrevitlib/pyrevit/loader/sessioninfo.py @@ -187,5 +187,3 @@ def report_env(): mlogger.info('Home Directory is: %s', HOME_DIR) mlogger.info('Session uuid is: %s', get_session_uuid()) mlogger.info('Runtime assembly is: %s', runtime.RUNTIME_ASSM_NAME) - mlogger.info('Config file is (%s): %s', - user_config.config_type, user_config.config_file) diff --git a/pyrevitlib/pyrevit/userconfig.py b/pyrevitlib/pyrevit/userconfig.py index cd0227846..5678cb02b 100644 --- a/pyrevitlib/pyrevit/userconfig.py +++ b/pyrevitlib/pyrevit/userconfig.py @@ -44,6 +44,7 @@ from pyrevit.compat import winreg as wr from pyrevit.labs import PyRevit +from pyrevit.labs import ConfigurationService from pyrevit import coreutils from pyrevit.coreutils import appdata @@ -61,16 +62,15 @@ CONSTS = PyRevit.PyRevitConsts -class PyRevitConfig(configparser.PyRevitConfigParser): +class PyRevitConfig(object): """Provide read/write access to pyRevit configuration. Args: - cfg_file_path (str): full path to config file to be used. - config_type (str): type of config file + config_service (IConfigurationService): configuration service. Examples: ```python - cfg = PyRevitConfig(cfg_file_path) + cfg = PyRevitConfig(config_service) cfg.add_section('sectionname') cfg.sectionname.property = value cfg.sectionname.get_option('property', default_value) @@ -78,16 +78,15 @@ class PyRevitConfig(configparser.PyRevitConfigParser): ``` """ - def __init__(self, cfg_file_path=None, config_type='Unknown'): - """Load settings from provided config file and setup parser.""" - # try opening and reading config file in order. - super(PyRevitConfig, self).__init__(cfg_file_path=cfg_file_path) - + def __init__(self, config_service): # set log mode on the logger module based on # user settings (overriding the defaults) + self.config_service = config_service + self._update_env() - self._admin = config_type == 'Admin' - self.config_type = config_type + + self._admin = self.config_service.ReadOnly + self.config_type = "Admin" if self.config_service.ReadOnly else "User" def _update_env(self): # update the debug level based on user config @@ -96,13 +95,13 @@ def _update_env(self): try: # first check to see if command is not in forced debug mode if not EXEC_PARAMS.debug_mode: - if self.core.debug: + if self.core.Debug: mlogger.set_debug_mode() mlogger.debug('Debug mode is enabled in user settings.') - elif self.core.verbose: + elif self.core.Verbose: mlogger.set_verbose_mode() - logger.set_file_logging(self.core.filelogging) + logger.set_file_logging(self.core.FileLogging) except Exception as env_update_err: mlogger.debug('Error updating env variable per user config. | %s', env_update_err) @@ -110,500 +109,309 @@ def _update_env(self): @property def config_file(self): """Current config file path.""" - return self._cfg_file_path + return PyRevit.PyRevitConsts.ConfigFilePath @property def environment(self): """Environment section.""" - if not self.has_section(CONSTS.EnvConfigsSectionName): - self.add_section(CONSTS.EnvConfigsSectionName) - return self.get_section(CONSTS.EnvConfigsSectionName) + return self.config_service.Environment @property def core(self): """Core section.""" - if not self.has_section(CONSTS.ConfigsCoreSection): - self.add_section(CONSTS.ConfigsCoreSection) - return self.get_section(CONSTS.ConfigsCoreSection) + return self.config_service.Core @property def routes(self): """Routes section.""" - if not self.has_section(CONSTS.ConfigsRoutesSection): - self.add_section(CONSTS.ConfigsRoutesSection) - return self.get_section(CONSTS.ConfigsRoutesSection) + return self.config_service.Routes @property def telemetry(self): """Telemetry section.""" - if not self.has_section(CONSTS.ConfigsTelemetrySection): - self.add_section(CONSTS.ConfigsTelemetrySection) - return self.get_section(CONSTS.ConfigsTelemetrySection) + return self.config_service.Telemetry @property def bin_cache(self): """"Whether to use the cache for extensions.""" - return self.core.get_option( - CONSTS.ConfigsBinaryCacheKey, - default_value=CONSTS.ConfigsBinaryCacheDefault, - ) + return self.core.BinCache @bin_cache.setter def bin_cache(self, state): - self.core.set_option( - CONSTS.ConfigsBinaryCacheKey, - value=state - ) + self.core.BinCache = state @property def check_updates(self): """Whether to check for updates.""" - return self.core.get_option( - CONSTS.ConfigsCheckUpdatesKey, - default_value=CONSTS.ConfigsCheckUpdatesDefault, - ) + return self.core.CheckUpdates @check_updates.setter def check_updates(self, state): - self.core.set_option( - CONSTS.ConfigsCheckUpdatesKey, - value=state - ) + self.core.CheckUpdates = state @property def auto_update(self): """Whether to automatically update pyRevit.""" - return self.core.get_option( - CONSTS.ConfigsAutoUpdateKey, - default_value=CONSTS.ConfigsAutoUpdateDefault, - ) + return self.core.AutoUpdate @auto_update.setter def auto_update(self, state): - self.core.set_option( - CONSTS.ConfigsAutoUpdateKey, - value=state - ) + self.core.AutoUpdate = state @property def rocket_mode(self): """Whether to enable rocket mode.""" - return self.core.get_option( - CONSTS.ConfigsRocketModeKey, - default_value=CONSTS.ConfigsRocketModeDefault, - ) + return self.core.RocketMode @rocket_mode.setter def rocket_mode(self, state): - self.core.set_option( - CONSTS.ConfigsRocketModeKey, - value=state - ) + self.core.RocketMode = state @property def log_level(self): """Logging level.""" - if self.core.get_option( - CONSTS.ConfigsDebugKey, - default_value=CONSTS.ConfigsDebugDefault, - ): + if self.core.Debug: return PyRevit.PyRevitLogLevels.Debug - elif self.core.get_option( - CONSTS.ConfigsVerboseKey, - default_value=CONSTS.ConfigsVerboseDefault, - ): + elif self.core.Verbose: return PyRevit.PyRevitLogLevels.Verbose return PyRevit.PyRevitLogLevels.Quiet @log_level.setter def log_level(self, state): if state == PyRevit.PyRevitLogLevels.Debug: - self.core.set_option(CONSTS.ConfigsDebugKey, True) - self.core.set_option(CONSTS.ConfigsVerboseKey, True) + self.core.Debug = True + self.core.Verbose = True elif state == PyRevit.PyRevitLogLevels.Verbose: - self.core.set_option(CONSTS.ConfigsDebugKey, False) - self.core.set_option(CONSTS.ConfigsVerboseKey, True) + self.core.Debug = False + self.core.Verbose = True else: - self.core.set_option(CONSTS.ConfigsDebugKey, False) - self.core.set_option(CONSTS.ConfigsVerboseKey, False) + self.core.Debug = False + self.core.Verbose = False @property def file_logging(self): """Whether to enable file logging.""" - return self.core.get_option( - CONSTS.ConfigsFileLoggingKey, - default_value=CONSTS.ConfigsFileLoggingDefault, - ) + return self.core.FileLogging @file_logging.setter def file_logging(self, state): - self.core.set_option( - CONSTS.ConfigsFileLoggingKey, - value=state - ) + self.core.FileLogging = state @property def startuplog_timeout(self): """Timeout for the startup log.""" - return self.core.get_option( - CONSTS.ConfigsStartupLogTimeoutKey, - default_value=CONSTS.ConfigsStartupLogTimeoutDefault, - ) + return self.core.StartupLogTimeout @startuplog_timeout.setter def startuplog_timeout(self, timeout): - self.core.set_option( - CONSTS.ConfigsStartupLogTimeoutKey, - value=timeout - ) + self.core.StartupLogTimeout = timeout @property def required_host_build(self): """Host build required to run the commands.""" - return self.core.get_option( - CONSTS.ConfigsRequiredHostBuildKey, - default_value="", - ) + return self.core.RequiredHostBuild @required_host_build.setter def required_host_build(self, buildnumber): - self.core.set_option( - CONSTS.ConfigsRequiredHostBuildKey, - value=buildnumber - ) + self.core.RequiredHostBuild = buildnumber @property def min_host_drivefreespace(self): """Minimum free space for running the commands.""" - return self.core.get_option( - CONSTS.ConfigsMinDriveSpaceKey, - default_value=CONSTS.ConfigsMinDriveSpaceDefault, - ) + return self.core.MinHostDriveFreeSpace @min_host_drivefreespace.setter def min_host_drivefreespace(self, freespace): - self.core.set_option( - CONSTS.ConfigsMinDriveSpaceKey, - value=freespace - ) + self.core.MinHostDriveFreeSpace = freespace @property def load_beta(self): """Whether to load commands in beta.""" - return self.core.get_option( - CONSTS.ConfigsLoadBetaKey, - default_value=CONSTS.ConfigsLoadBetaDefault, - ) + return self.core.LoadBeta @load_beta.setter def load_beta(self, state): - self.core.set_option( - CONSTS.ConfigsLoadBetaKey, - value=state - ) + self.core.LoadBeta = state @property def cpython_engine_version(self): """CPython engine version to use.""" - return self.core.get_option( - CONSTS.ConfigsCPythonEngineKey, - default_value=CONSTS.ConfigsCPythonEngineDefault, - ) + return self.core.CpythonEngineVersion @cpython_engine_version.setter def cpython_engine_version(self, version): - self.core.set_option( - CONSTS.ConfigsCPythonEngineKey, - value=int(version) - ) + self.core.CpythonEngineVersion = int(version) @property def user_locale(self): """User locale.""" - return self.core.get_option( - CONSTS.ConfigsLocaleKey, - default_value="", - ) + return self.core.UserLocale @user_locale.setter def user_locale(self, local_code): - self.core.set_option( - CONSTS.ConfigsLocaleKey, - value=local_code - ) + self.core.UserLocale = local_code @property def output_stylesheet(self): """Stylesheet used for output.""" - return self.core.get_option( - CONSTS.ConfigsOutputStyleSheet, - default_value="", - ) + return self.core.OutputStyleSheet @output_stylesheet.setter def output_stylesheet(self, stylesheet_filepath): - if stylesheet_filepath: - self.core.set_option( - CONSTS.ConfigsOutputStyleSheet, - value=stylesheet_filepath - ) - else: - self.core.remove_option(CONSTS.ConfigsOutputStyleSheet) + self.core.OutputStyleSheet = stylesheet_filepath @property def routes_host(self): """Routes API host.""" - return self.routes.get_option( - CONSTS.ConfigsRoutesHostKey, - default_value=CONSTS.ConfigsRoutesHostDefault, - ) + return self.routes.Host @routes_host.setter def routes_host(self, routes_host): - self.routes.set_option( - CONSTS.ConfigsRoutesHostKey, - value=routes_host - ) + self.routes.Host = routes_host @property def routes_port(self): """API routes port.""" - return self.routes.get_option( - CONSTS.ConfigsRoutesPortKey, - default_value=CONSTS.ConfigsRoutesPortDefault, - ) + return self.routes.Port @routes_port.setter def routes_port(self, port): - self.routes.set_option( - CONSTS.ConfigsRoutesPortKey, - value=port - ) + self.routes.Port = port @property def load_core_api(self): """Whether to load pyRevit core api.""" - return self.routes.get_option( - CONSTS.ConfigsLoadCoreAPIKey, - default_value=CONSTS.ConfigsConfigsLoadCoreAPIDefault, - ) + return self.routes.LoadCoreApi @load_core_api.setter def load_core_api(self, state): - self.routes.set_option( - CONSTS.ConfigsLoadCoreAPIKey, - value=state - ) + self.routes.LoadCoreApi = state @property def telemetry_utc_timestamp(self): """Whether to use UTC timestamps in telemetry.""" - return self.telemetry.get_option( - CONSTS.ConfigsTelemetryUTCTimestampsKey, - default_value=CONSTS.ConfigsTelemetryUTCTimestampsDefault, - ) + return self.telemetry.TelemetryUseUtcTimeStamps @telemetry_utc_timestamp.setter def telemetry_utc_timestamp(self, state): - self.telemetry.set_option( - CONSTS.ConfigsTelemetryUTCTimestampsKey, - value=state - ) + self.telemetry.TelemetryUseUtcTimeStamps = state @property def telemetry_status(self): """Telemetry status.""" - return self.telemetry.get_option( - CONSTS.ConfigsTelemetryStatusKey, - default_value=CONSTS.ConfigsTelemetryStatusDefault, - ) + return self.telemetry.TelemetryStatus @telemetry_status.setter def telemetry_status(self, state): - self.telemetry.set_option( - CONSTS.ConfigsTelemetryStatusKey, - value=state - ) + self.telemetry.TelemetryStatus = state @property def telemetry_file_dir(self): """Telemetry file directory.""" - return self.telemetry.get_option( - CONSTS.ConfigsTelemetryFileDirKey, - default_value="", - ) + return self.telemetry.TelemetryFileDir @telemetry_file_dir.setter def telemetry_file_dir(self, filepath): - self.telemetry.set_option( - CONSTS.ConfigsTelemetryFileDirKey, - value=filepath - ) + self.telemetry.TelemetryFileDir = filepath @property def telemetry_server_url(self): """Telemetry server URL.""" - return self.telemetry.get_option( - CONSTS.ConfigsTelemetryServerUrlKey, - default_value="", - ) + return self.telemetry.TelemetryServerUrl @telemetry_server_url.setter def telemetry_server_url(self, server_url): - self.telemetry.set_option( - CONSTS.ConfigsTelemetryServerUrlKey, - value=server_url - ) + self.telemetry.TelemetryServerUrl = server_url @property def telemetry_include_hooks(self): """Whether to include hooks in telemetry.""" - return self.telemetry.get_option( - CONSTS.ConfigsTelemetryIncludeHooksKey, - default_value=CONSTS.ConfigsTelemetryIncludeHooksDefault, - ) + return self.telemetry.TelemetryIncludeHooks @telemetry_include_hooks.setter def telemetry_include_hooks(self, state): - self.telemetry.set_option( - CONSTS.ConfigsTelemetryIncludeHooksKey, - value=state - ) + self.telemetry.TelemetryIncludeHooks = state @property def apptelemetry_status(self): """Telemetry status.""" - return self.telemetry.get_option( - CONSTS.ConfigsAppTelemetryStatusKey, - default_value=CONSTS.ConfigsAppTelemetryStatusDefault, - ) + return self.telemetry.AppTelemetryStatus @apptelemetry_status.setter def apptelemetry_status(self, state): - self.telemetry.set_option( - CONSTS.ConfigsAppTelemetryStatusKey, - value=state - ) + self.telemetry.AppTelemetryStatus = state @property def apptelemetry_server_url(self): """App telemetry server URL.""" - return self.telemetry.get_option( - CONSTS.ConfigsAppTelemetryServerUrlKey, - default_value="", - ) + return self.telemetry.AppTelemetryServerUrl @apptelemetry_server_url.setter def apptelemetry_server_url(self, server_url): - self.telemetry.set_option( - CONSTS.ConfigsAppTelemetryServerUrlKey, - value=server_url - ) + self.telemetry.AppTelemetryServerUrl = server_url @property def apptelemetry_event_flags(self): """Telemetry event flags.""" - return self.telemetry.get_option( - CONSTS.ConfigsAppTelemetryEventFlagsKey, - default_value="", - ) + return '{:x}'.format(self.telemetry.AppTelemetryEventFlags) @apptelemetry_event_flags.setter def apptelemetry_event_flags(self, flags): - self.telemetry.set_option( - CONSTS.ConfigsAppTelemetryEventFlagsKey, - value=flags - ) + self.telemetry.AppTelemetryEventFlags = int(flags) @property def user_can_update(self): """Whether the user can update pyRevit repos.""" - return self.core.get_option( - CONSTS.ConfigsUserCanUpdateKey, - default_value=CONSTS.ConfigsUserCanUpdateDefault, - ) + return self.core.UserCanUpdate @user_can_update.setter def user_can_update(self, state): - self.core.set_option( - CONSTS.ConfigsUserCanUpdateKey, - value=state - ) + self.core.UserCanUpdate = state @property def user_can_extend(self): """Whether the user can manage pyRevit Extensions.""" - return self.core.get_option( - CONSTS.ConfigsUserCanExtendKey, - default_value=CONSTS.ConfigsUserCanExtendDefault, - ) + return self.core.UserCanExtend @user_can_extend.setter def user_can_extend(self, state): - self.core.set_option( - CONSTS.ConfigsUserCanExtendKey, - value=state - ) + self.core.UserCanExtend = state @property def user_can_config(self): """Whether the user can access the configuration.""" - return self.core.get_option( - CONSTS.ConfigsUserCanConfigKey, - default_value=CONSTS.ConfigsUserCanConfigDefault, - ) + return self.core.UserCanConfig @user_can_config.setter def user_can_config(self, state): - self.core.set_option( - CONSTS.ConfigsUserCanConfigKey, - value=state - ) + self.core.UserCanConfig = state @property def colorize_docs(self): """Whether to enable the document colorizer.""" - return self.core.get_option( - CONSTS.ConfigsColorizeDocsKey, - default_value=CONSTS.ConfigsColorizeDocsDefault, - ) + return self.ColorizeDocs @colorize_docs.setter def colorize_docs(self, state): - self.core.set_option( - CONSTS.ConfigsColorizeDocsKey, - value=state - ) + self.core.ColorizeDocs = state @property def tooltip_debug_info(self): """Whether to append debug info on tooltips.""" - return self.core.get_option( - CONSTS.ConfigsAppendTooltipExKey, - default_value=CONSTS.ConfigsAppendTooltipExDefault, - ) + return self.core.TooltipDebugInfo @tooltip_debug_info.setter def tooltip_debug_info(self, state): - self.core.set_option( - CONSTS.ConfigsAppendTooltipExKey, - value=state - ) + self.core.TooltipDebugInfo = state @property def routes_server(self): """Whether the server routes are enabled.""" - return self.routes.get_option( - CONSTS.ConfigsRoutesServerKey, - default_value=CONSTS.ConfigsRoutesServerDefault, - ) + return self.routes.Status @routes_server.setter def routes_server(self, state): - self.routes.set_option( - CONSTS.ConfigsRoutesServerKey, - value=state - ) + self.routes.Status = state @property def respect_language_direction(self): @@ -660,11 +468,7 @@ def get_ext_root_dirs(self): def get_ext_sources(self): """Return a list of extension definition source files.""" - ext_sources = self.environment.get_option( - CONSTS.EnvConfigsExtensionLookupSourcesKey, - default_value=[], - ) - return list(set(ext_sources)) + return list(set(self.environment.Sources)) def set_thirdparty_ext_root_dirs(self, path_list): """Updates list of external extension directories in config file. @@ -677,7 +481,7 @@ def set_thirdparty_ext_root_dirs(self, path_list): raise PyRevitException("Path \"%s\" does not exist." % ext_path) try: - self.core.userextensions = \ + self.core.UserExtensions = \ [op.normpath(x) for x in path_list] except Exception as write_err: mlogger.error('Error setting list of user extension folders. | %s', @@ -741,12 +545,10 @@ def is_readonly(self): def save_changes(self): """Save user config into associated config file.""" - if not self._admin and self.config_file: - try: - super(PyRevitConfig, self).save() - except Exception as save_err: - mlogger.error('Can not save user config to: %s | %s', - self.config_file, save_err) + if not self._admin: + self.config_service.SaveSection(ConfigurationService.DefaultConfigurationName, self.core) + self.config_service.SaveSection(ConfigurationService.DefaultConfigurationName, self.routes) + self.config_service.SaveSection(ConfigurationService.DefaultConfigurationName, self.telemetry) # adjust environment per user configurations self._update_env() @@ -770,101 +572,8 @@ def find_config_file(target_path): return PyRevit.PyRevitConsts.FindConfigFileInDirectory(target_path) -def verify_configs(config_file_path=None): - """Create a user settings file. - - if config_file_path is not provided, configs will be in memory only - - Args: - config_file_path (str, optional): config file full name and path - - Returns: - (pyrevit.userconfig.PyRevitConfig): pyRevit config file handler - """ - if config_file_path: - mlogger.debug('Creating default config file at: %s', config_file_path) - coreutils.touch(config_file_path) - - try: - parser = PyRevitConfig(cfg_file_path=config_file_path) - except Exception as read_err: - # can not create default user config file under appdata folder - mlogger.warning('Can not create config file under: %s | %s', - config_file_path, read_err) - parser = PyRevitConfig() - - return parser - - -LOCAL_CONFIG_FILE = ADMIN_CONFIG_FILE = USER_CONFIG_FILE = CONFIG_FILE = '' user_config = None # location for default pyRevit config files if not EXEC_PARAMS.doc_mode: - LOCAL_CONFIG_FILE = find_config_file(HOME_DIR) - ADMIN_CONFIG_FILE = find_config_file(PYREVIT_ALLUSER_APP_DIR) - USER_CONFIG_FILE = find_config_file(PYREVIT_APP_DIR) - - # decide which config file to use - # check if a config file is inside the repo. for developers config override - if LOCAL_CONFIG_FILE: - CONFIG_TYPE = 'Local' - CONFIG_FILE = LOCAL_CONFIG_FILE - - # check to see if there is any config file provided by admin - elif ADMIN_CONFIG_FILE \ - and os.access(ADMIN_CONFIG_FILE, os.W_OK) \ - and not USER_CONFIG_FILE: - # if yes, copy that and use as default - # if admin config file is writable it means it is provided - # to bootstrap the first pyRevit run - try: - CONFIG_TYPE = 'Seed' - # make a local copy if one does not exist - PyRevit.PyRevitConfigs.SetupConfig(ADMIN_CONFIG_FILE) - CONFIG_FILE = find_config_file(PYREVIT_APP_DIR) - except Exception as adminEx: - # if init operation failed, make a new config file - CONFIG_TYPE = 'New' - # setup config file name and path - CONFIG_FILE = appdata.get_universal_data_file(file_id='config', - file_ext='ini') - mlogger.warning( - 'Failed to initialize config from seed file at %s\n' - 'Using default config file', - ADMIN_CONFIG_FILE - ) - - # unless it's locked. then read that config file and set admin-mode - elif ADMIN_CONFIG_FILE \ - and not os.access(ADMIN_CONFIG_FILE, os.W_OK): - CONFIG_TYPE = 'Admin' - CONFIG_FILE = ADMIN_CONFIG_FILE - - # if a config file is available for user use that - elif USER_CONFIG_FILE: - CONFIG_TYPE = 'User' - CONFIG_FILE = USER_CONFIG_FILE - - # if nothing can be found, make a new one - else: - CONFIG_TYPE = 'New' - # setup config file name and path - CONFIG_FILE = appdata.get_universal_data_file(file_id='config', - file_ext='ini') - - mlogger.debug('Using %s config file: %s', CONFIG_TYPE, CONFIG_FILE) - - # read config, or setup default config file if not available - # this pushes reading settings at first import of this module. - try: - verify_configs(CONFIG_FILE) - user_config = PyRevitConfig(cfg_file_path=CONFIG_FILE, - config_type=CONFIG_TYPE) - upgrade.upgrade_user_config(user_config) - user_config.save_changes() - except Exception as cfg_err: - mlogger.debug('Can not read confing file at: %s | %s', - CONFIG_FILE, cfg_err) - mlogger.debug('Using configs in memory...') - user_config = verify_configs() + user_config = PyRevitConfig(PyRevit.PyRevitConfigs.GetConfigFile()) \ No newline at end of file From 09546816b1822065356ef166dac063f8da976c37 Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 10:42:40 +0300 Subject: [PATCH 30/37] update tests --- .../IniConfigurationUnitTests.cs | 2 +- .../JsonConfigurationUnitTests.cs | 2 +- .../YamlConfigurationUnitTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs index e9811b4ac..812a59cdb 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Ini.Tests/IniConfigurationUnitTests.cs @@ -31,7 +31,7 @@ public void CreateIniConfigurationByBuilder_ShouldThrowException() { Assert.Throws(() => { - new ConfigurationBuilder() + new ConfigurationBuilder(false) .AddIniConfiguration(default!, default!) .Build(); }); diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs index fa030ca17..4c74721f3 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Json.Tests/JsonConfigurationUnitTests.cs @@ -31,7 +31,7 @@ public void CreateJsonConfigurationByBuilder_ShouldThrowException() { Assert.Throws(() => { - new ConfigurationBuilder() + new ConfigurationBuilder(false) .AddJsonConfiguration(default!, default!) .Build(); }); diff --git a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs index a832294bb..aa09cd270 100644 --- a/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs +++ b/dev/pyRevitLabs/tests/pyRevitLabs.Configurations.Yaml.Tests/YamlConfigurationUnitTests.cs @@ -31,7 +31,7 @@ public void CreateYamlConfigurationByBuilder_ShouldThrowException() { Assert.Throws(() => { - new ConfigurationBuilder() + new ConfigurationBuilder(false) .AddYamlConfiguration(default!, default!) .Build(); }); From 23b218f9a041fd27211e8ccd0f26b5725497ee53 Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 15:19:03 +0300 Subject: [PATCH 31/37] add realization --- .../ConfigurationBase.cs | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs index 498736b6e..1a899b778 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationBase.cs @@ -46,11 +46,6 @@ public bool HasSectionKey(string sectionName, string keyName) return HasSectionKeyImpl(sectionName, keyName); } - public object? GetValueOrDefault(Type typeObject, string sectionName, string keyName, object? defaultValue = default) - { - throw new NotImplementedException(); - } - /// public bool RemoveValue(string sectionName, string keyName) { @@ -107,10 +102,39 @@ public T GetValue(string sectionName, string keyName) return (T) GetValueImpl(typeof(T), sectionName, keyName); } + + public object? GetValueOrDefault(Type typeObject, string sectionName, string keyName, object? defaultValue = default) + { + if (string.IsNullOrWhiteSpace(sectionName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(sectionName)); + + if (string.IsNullOrWhiteSpace(keyName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(keyName)); + + if (!HasSection(sectionName)) + return defaultValue; + + if (!HasSectionKey(sectionName, keyName)) + return defaultValue; + + return GetValueImpl(typeObject, sectionName, keyName); + } /// public object GetValue(Type typeObject, string sectionName, string keyName) { + if (string.IsNullOrWhiteSpace(sectionName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(sectionName)); + + if (string.IsNullOrWhiteSpace(keyName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(keyName)); + + if (!HasSection(sectionName)) + throw new ConfigurationSectionNotFoundException(sectionName); + + if (!HasSectionKey(sectionName, keyName)) + throw new ConfigurationSectionKeyNotFoundException(sectionName, keyName); + return GetValueImpl(typeObject, sectionName, keyName); } From c1fef094f5af96d4060c761cef1fa14c1a00bb65 Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 15:19:19 +0300 Subject: [PATCH 32/37] add reference --- pyrevitlib/pyrevit/labs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyrevitlib/pyrevit/labs.py b/pyrevitlib/pyrevit/labs.py index b0e8e3ca9..611dee2e2 100644 --- a/pyrevitlib/pyrevit/labs.py +++ b/pyrevitlib/pyrevit/labs.py @@ -38,6 +38,10 @@ clr.AddReference('pyRevitLabs.TargetApps.Revit') clr.AddReference('pyRevitLabs.PyRevit') clr.AddReference('PythonStubsBuilder') + +# configurations +clr.AddReference('pyRevitLabs.Configurations') + import Nett import MadMilkman.Ini import OpenMcdf @@ -52,6 +56,7 @@ from pyRevitLabs import DeffrelDB from pyRevitLabs import TargetApps from pyRevitLabs import PyRevit +from pyRevitLabs import Configurations from PythonStubs import PythonStubsBuilder from pyrevit import coreutils From 5c7dec2cb7cc890295d88c301578cb67c3739e94 Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 15:32:17 +0300 Subject: [PATCH 33/37] add revit version to config --- .../pyRevitLabs.PyRevit/PyRevitConfigs.cs | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs index b76fa5a6e..cd86e2fa8 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs @@ -168,7 +168,7 @@ public static void SetUTCStamps(bool state, { _logger.Debug("Setting telemetry utc timestamps to {@TelemetryStatus}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new TelemetrySection() {TelemetryStatus = state}); } @@ -184,7 +184,7 @@ public static void SetRoutesServerStatus(bool state, { _logger.Debug("Setting routes server status to {@Status}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new RoutesSection() {Status = state}); } @@ -205,7 +205,7 @@ public static void SetRoutesServerHost(string host, { _logger.Debug("Setting routes server host to {@Host}...", host); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new RoutesSection() {Host = host}); } @@ -220,7 +220,7 @@ public static void SetRoutesServerPort(int port, { _logger.Debug("Setting routes server port to {@Port}...", port); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new RoutesSection() {Port = port}); } @@ -235,7 +235,7 @@ public static void SetRoutesLoadCoreAPIStatus(bool state, { _logger.Debug("Setting routes load core API status to {@LoadCoreApi}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new RoutesSection() {LoadCoreApi = state}); } @@ -251,7 +251,7 @@ public static void SetTelemetryStatus(bool state, { _logger.Debug("Setting telemetry status to {@TelemetryStatus}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new TelemetrySection() {TelemetryStatus = state}); } @@ -279,7 +279,7 @@ public static void EnableTelemetry(string telemetryFileDir = null, telemetryFileDir = default; } - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new TelemetrySection() { @@ -300,7 +300,7 @@ public static void SetTelemetryIncludeHooks(bool state, { _logger.Debug("Setting telemetry include hooks to {@TelemetryIncludeHooks}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new TelemetrySection() {TelemetryIncludeHooks = state}); } @@ -308,7 +308,7 @@ public static void DisableTelemetry(string revitVersion = ConfigurationService.D { _logger.Debug("Disabling telemetry..."); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new TelemetrySection() {TelemetryStatus = false}); } @@ -324,7 +324,7 @@ public static void SetAppTelemetryStatus(bool state, { _logger.Debug("Setting app telemetry status to {@AppTelemetryStatus}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new TelemetrySection() {AppTelemetryStatus = state}); } @@ -339,7 +339,7 @@ public static void EnableAppTelemetry(string apptelemetryServerUrl = null, { _logger.Debug("Enabling app telemetry..."); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new TelemetrySection() {AppTelemetryServerUrl = apptelemetryServerUrl}); } @@ -347,7 +347,7 @@ public static void DisableAppTelemetry(string revitVersion = ConfigurationServic { _logger.Debug("Disabling app telemetry..."); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new TelemetrySection() {AppTelemetryStatus = false}); } @@ -362,7 +362,7 @@ public static void SetAppTelemetryFlags(string flags, { _logger.Debug("Setting app telemetry flags to {@AppTelemetryEventFlags}...", flags); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new TelemetrySection() {AppTelemetryEventFlags = int.Parse(flags, System.Globalization.NumberStyles.HexNumber)}); @@ -380,7 +380,7 @@ public static void SetBinaryCaches(bool state, { _logger.Debug("Setting binary caches {@BinCache}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {BinCache = state}); } @@ -396,7 +396,7 @@ public static void SetCheckUpdates(bool state, { _logger.Debug("Setting check updates to {@CheckUpdates}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {CheckUpdates = state}); } @@ -412,7 +412,7 @@ public static void SetAutoUpdate(bool state, { _logger.Debug("Setting auto update to {@AutoUpdate}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {AutoUpdate = state}); } @@ -428,7 +428,7 @@ public static void SetRocketMode(bool state, { _logger.Debug("Setting rocket mode to {@RocketMode}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {RocketMode = state}); } @@ -451,7 +451,7 @@ public static void SetLoggingLevel(PyRevitLogLevels level, { _logger.Debug("Setting logging level to {@LogLevel}...", level); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); if (level == PyRevitLogLevels.Quiet) { cfg.SaveSection(revitVersion, new CoreSection() {Debug = false, Verbose = false}); @@ -478,7 +478,7 @@ public static void SetFileLogging(bool state, { _logger.Debug("Setting file logging to {@FileLogging}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {FileLogging = state}); } @@ -494,7 +494,7 @@ public static void SetStartupLogTimeout(int timeout, { _logger.Debug("Setting startup log timeout to {@StartupLogTimeout}...", timeout); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {StartupLogTimeout = timeout}); } @@ -509,7 +509,7 @@ public static void SetRequiredHostBuild(string buildnumber, { _logger.Debug("Setting required host build to {@RequiredHostBuild}...", buildnumber); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {RequiredHostBuild = buildnumber}); } @@ -524,7 +524,7 @@ public static void SetMinHostDriveFreeSpace(long freespace, { _logger.Debug("Setting min host drive free space to {@MinHostDriveFreeSpace}...", freespace); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {MinHostDriveFreeSpace = freespace}); } @@ -540,7 +540,7 @@ public static void SetLoadBetaTools(bool state, { _logger.Debug("Setting load beta tools to {@LoadBeta}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {LoadBeta = state}); } @@ -556,7 +556,7 @@ public static void SetCpythonEngineVersion(int version, { _logger.Debug("Setting cpyhon engine version to {@CpythonEngineVersion}...", version); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {CpythonEngineVersion = version}); } @@ -572,7 +572,7 @@ public static void SetUserLocale(string localCode, { _logger.Debug("Setting user locale to {@LocalCode}...", localCode); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {UserLocale = localCode}); } @@ -587,7 +587,7 @@ public static void SetOutputStyleSheet(string outputCssFilePath, { _logger.Debug("Setting output style sheet to {@OutputCssFilePath}...", outputCssFilePath); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); if (File.Exists(outputCssFilePath)) cfg.SaveSection(revitVersion, new CoreSection() {OutputStyleSheet = outputCssFilePath}); } @@ -616,7 +616,7 @@ public static void SetUserCanUpdate(bool state, { _logger.Debug("Setting user can install to {@UserCanUpdate}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {UserCanUpdate = state}); } @@ -625,7 +625,7 @@ public static void SetUserCanExtend(bool state, { _logger.Debug("Setting user can install to {@UserCanExtend}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {UserCanExtend = state}); } @@ -634,7 +634,7 @@ public static void SetUserCanConfig(bool state, { _logger.Debug("Setting user can install to {@UserCanConfig}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {UserCanConfig = state}); } @@ -649,7 +649,7 @@ public static void SetColorizeDocs(bool state, { _logger.Debug("Setting colorize docs to {@ColorizeDocs}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {ColorizeDocs = state}); } @@ -664,7 +664,7 @@ public static void SetAppendTooltipEx(bool state, { _logger.Debug("Setting tooltip debug info to {@TooltipDebugInfo}...", state); - IConfigurationService cfg = GetConfigFile(); + IConfigurationService cfg = GetConfigFile(revitVersion); cfg.SaveSection(revitVersion, new CoreSection() {TooltipDebugInfo = state}); } } From df9de229f25d614e1302bc31d51cb83483eca6c6 Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 15:33:37 +0300 Subject: [PATCH 34/37] fix override config name --- dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs index cd86e2fa8..556d79ae0 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConfigs.cs @@ -149,7 +149,7 @@ private static IConfigurationService CreateConfiguration(string configPath, bool { builder.AddIniConfiguration( Path.ChangeExtension(configPath, - $"{overrideName}.{IniConfiguration.DefaultFileExtension}"), overrideName, readOnly); + $"{overrideName}{IniConfiguration.DefaultFileExtension}"), overrideName, readOnly); } return builder.Build(); From 63759938436a7ac2b8fdef1bf6dcc31bdffbb7dd Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 15:36:24 +0300 Subject: [PATCH 35/37] fix override config prioritization --- .../pyRevitLabs.Configurations/ConfigurationService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs index 4fe1db1fe..2292fb389 100644 --- a/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs +++ b/dev/pyRevitLabs/pyRevitLabs.Configurations/ConfigurationService.cs @@ -57,7 +57,7 @@ private void LoadConfigurations() public T GetSection() { Type configurationType = typeof(T); - return (T) CreateSection(configurationType, Configurations.ToArray()); + return (T) CreateSection(configurationType, Configurations.Reverse().ToArray()); } public void SaveSection(string configurationName, T sectionValue) From 838f1635a0de431808ce2b1b1ee088a65dd74a22 Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 16:22:35 +0300 Subject: [PATCH 36/37] update references --- pyrevitlib/pyrevit/labs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyrevitlib/pyrevit/labs.py b/pyrevitlib/pyrevit/labs.py index 611dee2e2..062a9e022 100644 --- a/pyrevitlib/pyrevit/labs.py +++ b/pyrevitlib/pyrevit/labs.py @@ -9,7 +9,6 @@ # try loading pyrevitlabs clr.AddReference('Nett') -clr.AddReference('MadMilkman.Ini') clr.AddReference('OpenMcdf') clr.AddReference('YamlDotNet') clr.AddReference('pyRevitLabs.NLog') @@ -43,7 +42,6 @@ clr.AddReference('pyRevitLabs.Configurations') import Nett -import MadMilkman.Ini import OpenMcdf import YamlDotNet as libyaml import pyRevitLabs.MahAppsMetro @@ -56,9 +54,11 @@ from pyRevitLabs import DeffrelDB from pyRevitLabs import TargetApps from pyRevitLabs import PyRevit -from pyRevitLabs import Configurations from PythonStubs import PythonStubsBuilder +from pyRevitLabs import Configurations +from pyRevitLabs.Configurations import ConfigurationService + from pyrevit import coreutils from pyrevit.coreutils import logger From eae821aa9be995a4949cba2f06cbde7499598c49 Mon Sep 17 00:00:00 2001 From: dosymep Date: Sat, 28 Dec 2024 16:23:48 +0300 Subject: [PATCH 37/37] remove references --- pyrevitlib/pyrevit/userconfig.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyrevitlib/pyrevit/userconfig.py b/pyrevitlib/pyrevit/userconfig.py index 5678cb02b..712ebec65 100644 --- a/pyrevitlib/pyrevit/userconfig.py +++ b/pyrevitlib/pyrevit/userconfig.py @@ -40,17 +40,13 @@ from pyrevit import EXEC_PARAMS, HOME_DIR, HOST_APP from pyrevit import PyRevitException from pyrevit import EXTENSIONS_DEFAULT_DIR, THIRDPARTY_EXTENSIONS_DEFAULT_DIR -from pyrevit import PYREVIT_ALLUSER_APP_DIR, PYREVIT_APP_DIR from pyrevit.compat import winreg as wr from pyrevit.labs import PyRevit from pyrevit.labs import ConfigurationService from pyrevit import coreutils -from pyrevit.coreutils import appdata -from pyrevit.coreutils import configparser from pyrevit.coreutils import logger -from pyrevit.versionmgr import upgrade DEFAULT_CSV_SEPARATOR = ','