Monday, November 18, 2019

Learning Google Cloud Platform


If you haven't worked or learned about Google Cloud Platform (GCP) before, and want to learn everything about it - leave all hope behind you.

However, if you'd like to start a journey into this exciting technology, here are a few pointers to start from:

First, check this one:


and follow the steps, from
  • Quick intros,
  • In-depth courses,
  • Role-based learning paths,
  • Practice and play, to
  • Certification.
You can try free tutorials, video courses, hands-on labs, or find paid on-demand or courses, depending on your learning needs and preferences.

Once you get past quick intros and decide to follow either role based or certification paths, you'll can choose between

  • DevOps,
  • Architecture,
  • Application developer,
  • Data Engineer / Data Scientist, or
  • Machine Learning tracks.

If you prefer reading most update and correct material, head to documentation site:


Within Google, GCP console provides interactive tutorials: activate help (?), select 'Start a tutorial', and continue with the Interactive tutorials panel.
GCP console also provides 'Take the quickstart' button in a number of tasks done first time.

Outside Google, there are several online options providing video courses, hands-on labs, quizzes, supporting materials and certifications (proprietary, not Google - for Google Certified Program, check https://cloud.google.com/certification).

https://www.coursera.org  was one of the first available options for learning GCP, with courses developed by Google, along with curated and tailored hands-on labs provided by QuikLabs.

Courses can be taken individually, or as part of specializations. You can walk through the courses without subscription, without access quizzes (results) and labs. You can also use handy free trials to run through the courses, labs and quizzes, before buying a subscription.

A Cloud Guru started with training curriculum for AWS, but expanded to GCP and Azure. They also provide quizzes, hands-on labs and certifications.

https://linuxacademy.com started with courses on Linux, containers, DevOps, and provided servers to play with. They were one of the first to include AWS training, and later GCP and Azure. They also provide access to AWS, GCP and Azure cloud resources, that is used to run the curated hands-on labs within the courses or learn-by-doing exercises, but also for limited ad hoc experimentation. They also have tests, and other learning resources.

https://www.udemy.com provides hours of discounted video courses that also include practice tests.

https://www.pluralsight.com is an excellent source of courses, particularly useful if you already have access to it, or need a one stop shop for your training needs. In addition to courses in different formats, they provide live events, and self-assessment tools (roleIQ).
For a number of years, there were just a handful of courses on GCP. In 2019, a number of courses from Udemy were also published on Pluralsight, and after Google and Pluralsight partnered, most of the Google authored courses for Coursera were published on Pluralsight. These courses include the same credited access to the hands-on labs in QuikLabs.

Finally, https://www.qwiklabs.com is another resource particularly fitting the learners who like to learn by action. Here you can run individual labs, or embark on quests, series of labs gradually introducing and covering ranges of topics. You need to sign up and get credits to run the labs. Note that the credits are included in the courses authored by Google on Coursera and Pluralsight.

All the listed sites provide different trial modes, subscription terms or individual course purchases, and I recommend trying at least two. You may even continue with more than one, it makes it more interesting and easier learning, when listening to the same topic from different people.

There are other resources for which you already have subscription to and I would not mention here, assuming that you already checked them out. Coming to mind are online training at https://www.oreilly.com, (remember Safari Books Online? Remember books?...)

One final note: Cloud tech changes quickly and make sure you are looking at the most update version of the course.

Happy learning.


Friday, August 8, 2014

JSON.NET Serialization of NameValueCollection

Yesterday, a colleague of mine was having problems implementing a Web API microservice, where client was throwing the exception:

'Cannot create and populate list type System.Collections.Specialized.NameValueCollection. Path ...' on de-serialization of a complex graph containing NameValueCollections.

Web searches returned a number suggestions to replace the NameValueCollection with a dictionary, which did not work in our case, where we needed to use classes as is.

As I've created the initial reference architecture, I felt obliged to help resolve the issue. As said, Web searches for a solution didn't return anything closely applicable, I've taken a bit deeper look at the Json.NET library documentation, and shortly afterwards decided to create a custom converter.

It turned out to be easy. Here's the source code:
/// <summary>
/// Custom converter for (de)serializing NameValueCollection
/// Add an instance to the settings Converters collection
/// </summary>
public class NameValueCollectionConverter : JsonConverter
{
  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    var collection = value as NameValueCollection;
    if (collection == null)
      return;

    writer.WriteStartObject();
    foreach (var key in collection.AllKeys)
    {
      writer.WritePropertyName(key);
      writer.WriteValue(collection.Get(key));
    }
    writer.WriteEndObject();
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    var nameValueCollection = new NameValueCollection();
    var key = "";
    while (reader.Read())
    {
      if (reader.TokenType == JsonToken.StartObject)
      {
        nameValueCollection = new NameValueCollection();
      }
      if (reader.TokenType == JsonToken.EndObject)
      {
        return nameValueCollection;                    
      }
      if (reader.TokenType == JsonToken.PropertyName)
      {
        key = reader.Value.ToString();
      }
      if (reader.TokenType == JsonToken.String)
      {
        nameValueCollection.Add(key, reader.Value.ToString());
      }
    }
    return nameValueCollection;
  }

  public override bool CanConvert(Type objectType)
  {
    return objectType == typeof(NameValueCollection);
  }
}

To user the converter, add it to the converters collection as follows:

var settings = new JsonSerializerSettings()
  {
    ContractResolver = new CamelCasePropertyNamesContractResolver(),
    TypeNameHandling = TypeNameHandling.All,
    Formatting = Formatting.Indented
  };
  settings.Converters.Add(new NameValueCollectionConverter());
  JsonConvert.DefaultSettings = () => settings;
Nice job done in implementing and documenting Json.NET!

Wednesday, December 11, 2013

Fix HL7 ORU_R01_ORDER_OBSERVATION in NHapi

This week, I worked on a feature requiring parsing of HL7 lab test results messages (ORU_R01).
Some messages did not parse correctly, and trusting the NHapi library http://sourceforge.net/projects/nhapi/,
I've spent some time tinkering around the problem, namely being unable to parse multiple observations (OBX segment). In the API, OBSERVATION was for a single instance property.
Finding no ways further, I've downloaded the source and took a look inside. The code is structured fairly good and it was easy to compare with other HL7 API documentation, especially http://hl7api.sourceforge.net/v251/apidocs/ca/uhn/hl7v2/model/v251/group/ORU_R01_ORDER_OBSERVATION.html.

There were two issues:
- ORU_R01_OBSERVATION has ORU_R01_OBSERVATION group incorrectly defined as required, non-repeating. It should be optional, repeating. Similarly,
- ORU_R01_SPECIMEN group is defined as required, non-repeating, but should be optional, repeating as well.

Here is a simple code change in the group constructor:

///<summary>
/// Creates new ORU_R01_ORDER_OBSERVATION Group.
///</summary>
public ORU_R01_ORDER_OBSERVATION(IGroup parent, IModelClassFactory factory) : base(parent, factory)
{
  try {
    this.add(typeof(ORC), false, false);
    this.add(typeof(OBR, true, false);
    this.add(typeof(NTE), false, true);
    this.add(typeof(ORU_R01_TIMING_QTY), true, false<);
    this.add(typeof(CTD), false, false);
    this.add(typeof(ORU_R01_OBSERVATION), false, true); //TODO: make ORU_R01_OBSERVATION optional, repeating - was true, false
    this.add(typeof(FT1), false, true);
    this.add(typeof(CTI), false, true);
    this.add(typeof(ORU_R01_SPECIMEN), false, true); //TODO: make ORU_R01_SPECIMEN optional, repeating - was true,false
  }
  catch(HL7Exception e)
  {
    HapiLogFactory.GetHapiLog(GetType()).Error("Unexpected error creating ORU_R01_ORDER_OBSERVATION - this is probably a bug in the source code generator.", e);
  }
}

Also, single instance properties needed change to repetition access methods as follows:

//TODO: replace OBSERVATION instance accessor with collection
//  ///<summary>
//  /// Returns ORU_R01_OBSERVATION(a Group object) - creates it if necessary
//    ///</summary>
//    public ORU_R01_OBSERVATION OBSERVATION { 
//       get{
//       ORU_R01_OBSERVATION ret = null;
//       try {
//          ret = (ORU_R01_OBSERVATION)this.GetStructure("OBSERVATION");
//       } catch(HL7Exception e) {
//          HapiLogFactory.GetHapiLog(GetType()).Error("Unexpected error accessing data - this is probably a bug in the source code generator.", e);
//          throw new System.Exception("An unexpected error ocurred",e);
//       }
//       return ret;
//    }
//    }
    ///<summary>
    ///  Returns  first repetition of ORU_R01_OBSERVATION (a Group object) - creates it if necessary 
    ///</summary> 
    public ORU_R01_OBSERVATION GetOBSERVATION ()
    {
      ORU_R01_OBSERVATION ret = null;
        try 
        {
            ret = (ORU_R01_OBSERVATION)this.GetStructure("OBSERVATION");
        }
        catch (HL7Exception e)
        {
            HapiLogFactory.GetHapiLog(GetType()).Error("Unexpected error accessing data - this is probably a bug in the source code generator." , e);
            throw new System.Exception("An unexpected error ocurred", e);
        }
        return ret;
    }
 
     ///<summary> 
     ///  Returns a specific repetition of OUL_R21_OBSERVATION 
     ///   * (a Group object) - creates it if necessary 
     ///   throws HL7Exception if the repetition requested is more than one  
     ///       greater than the number of existing repetitions. 
     ///</summary> 
    public ORU_R01_OBSERVATION GetOBSERVATION(int rep)
    {
        return (ORU_R01_OBSERVATION)this.GetStructure("OBSERVATION", rep);
    }
 
     /**  
      * Returns the number of existing repetitions of OUL_R21_OBSERVATION  
      */ 
    public int OBSERVATIONRepetitionsUsed
    {
        get 
        {
            int reps = -1;
            try 
            {
                reps = this.GetAll("OBSERVATION").Length;
            }
            catch (HL7Exception e)
            {
                string message = "Unexpected error accessing data - this is probably a bug in the source code generator.";
                HapiLogFactory.GetHapiLog(GetType ()).Error(message, e);
                throw new System.Exception(message);
            }
            return reps;
        }
    }

and
//TODO: replace SPECIMEN instance accessor with collection
//    ///<summary>
//    /// Returns ORU_R01_SPECIMEN (a Group object) - creates it if necessary
//    ///</summary>
//    public ORU_R01_SPECIMEN SPECIMEN { 
//    get {
//       ORU_R01_SPECIMEN ret = null;
//       try {
//          ret = (ORU_R01_SPECIMEN)this.GetStructure("SPECIMEN");
//       } catch(HL7Exception e) {
//          HapiLogFactory.GetHapiLog(GetType()).Error("Unexpected error accessing data - this is probably a bug in the source code generator.", e);
//          throw new System.Exception("An unexpected error ocurred",e);
//       }
//       return ret;
//      }
//    }
    ///<summary>
    /// Returns  first repetition of ORU_R01_SPECIMEN (a Group object) - creates it if necessary
    ///</summary>
    public ORU_R01_SPECIMEN GetSPECIMEN()
    {
        ORU_R01_SPECIMEN ret = null;
        try
        {
            ret = (ORU_R01_SPECIMEN)this.GetStructure("SPECIMEN");
        }
        catch (HL7Exception e)
        {
            HapiLogFactory.GetHapiLog(GetType()).Error("Unexpected error accessing data - this is probably a bug in the source code generator.", e);
            throw new System.Exception("An unexpected error ocurred", e);
        }
        return ret;
    }
 
    ///<summary>
    ///Returns a specific repetition of ORU_R01_SPECIMEN
    /// * (a Group object) - creates it if necessary
    /// throws HL7Exception if the repetition requested is more than one 
    ///     greater than the number of existing repetitions.
    ///</summary>
    public ORU_R01_SPECIMEN GetSPECIMEN(int rep)
    {
        return (ORU_R01_SPECIMEN)this.GetStructure("SPECIMEN", rep);
    }
 
    /** 
     * Returns the number of existing repetitions of ORU_R01_SPECIMEN 
     */
    public int SPECIMENRepetitionsUsed
    {
        get
        {
            int reps = -1;
            try
            {
                reps = this.GetAll("SPECIMEN").Length;
            }
            catch (HL7Exception e)
            {
                string message = "Unexpected error accessing data - this is probably a bug in the source code generator.";
                HapiLogFactory.GetHapiLog(GetType()).Error(message, e);
                throw new System.Exception(message);
            }
            return reps;
        }
    }


Offered to contribute the change back to https://sourceforge.net/projects/nhapi/

Wednesday, October 2, 2013

Setting INSTALLFOLDER in Custom Action

Yesterday, I've looked at changing INSTALLFOLDER in a custom action for a WIX 3.7 installer project.

As installer technology is not my usual daily thing, the Web was the first resource. I've looked at a few of the related posts, where most of them advising that the action should be invoked after CostInitialize step or similar. This might be useful for other properties, but not for INSTALLFOLDER, as the installer apparently used the default value and installed the product in the default location.

After trying a few spots in the InstallExecuteSequence with the same result, I've ultimately logged the install run, which showed that the custom action was invoked too late, i.e. after target locations for the components were already set based on the default INSTALLFOLDER.

After looking at Suggested InstallExecuteSequence, on the MSDN library site, I've decided to execute it at the earliest available location i.e. before LaunchConditions. And it worked!

Below is the snippet of the code from the .wxs file

<CustomAction Id="SetINSTALLFOLDER" BinaryKey="CustomActions" DllEntry="SetINSTALLFOLDER" Execute="immediate" Return="check"  />
<InstallExecuteSequence>
 <Custom Action="SetINSTALLFOLDER" Before="LaunchConditions">NOT Installed<Custom>
</InstallExecuteSequence>

and here is a snippet of the code in the custom action:

public class CustomActions
{
  [CustomAction]
  public static ActionResult SetINSTALLFOLDER(Session session)
  {
    try
    {
      var installFolder = ... // defined here
      if (!string.IsNullOrEmpty(installFolder))
      {
        session["INSTALLFOLDER"] = installFolder; 
        session.Log("SetINSTALLFOLDER: set to '{0}'", session["INSTALLFOLDER"]);
      }
      return ActionResult.Success;
    }
    catch (Exception ex)
    {
      session.Log("SetINSTALLFOLDER exception: {0}", ex.Message);
      return ActionResult.Failure;
    }
  }
}

The thing that I don't like is that it also overrides user overrides via msiexec command line arguments, but it wasn't an issue this time...