• How to Resolve Overlapping DjangoObjectTypes with Graphene and Graphene Relay

    If your schema has multiple types defined using DjangoObjectType for the same model in a Union, selections that work on the Union won't necessarily work as is on Relay node queries.Β 

    For example, suppose we had a model schema like this:

    class Record(models.Model):
        record_type = models.CharField(max_length=12)
        
        @property
        def is_disco(self) -> bool:
            return self.record_type == "DISCO"
    
        @property
        def is_punk(self) -> bool:
            return self.record_type == "PUNK"
    
    class Disco(models.Model):
        record = models.OneToOneField(Record, related_name="disco_record")
        bpm = models.IntegerField()
    
    
    class Punk(models.Model):
        record = models.OneToOneField(Record, related_name="punk_record")
        max_chords = models.IntegerField()

    Our application cares Records and, depending on the record_type, the type of meta information we want to manage changes. As such we create a new model with a OneToOneField to our record for each type we plan on managing.

    When we query our records we wan to only worry about Records, so define our GraphQL types accordingly.

    class DiscoRecord(graphene_django.DjangoObjectType):
        class Meta:
            model = models.Record
        
        bpm = graphene.IntegerField(required=True)
    
        @classmethod
        def get_node(cls, info, id) -> models.Record:
            # Allow our object to be fetchable as a Relay Node
            return models.Record.objects.get(pk=id)
        
        def resolve_bpm(record: models.Record, **kwargs) -> int:
            return record.disco_record.bpm
    
    class PunkRecord(graphene_django.DjangoObjectType):
        class Meta:
            model = models.Record
        
        max_chords = graphene.IntegerField(required=True)
        
        @classmethod
        def get_node(cls, info, id) -> models.Record:
            # Allow our object to be fetchable as a Relay Node
            return models.Record.objects.get(pk=id)
        
        def resolve_max_chords(record: models.Record, **kwargs) -> int:
            return record.punk_record.max_chords
    
    
    class Record(graphene.Union):
        class Meta:
            types = (DiscoRecord, PunkRecord)
    
        @classmethod
        def resolve_type(
            cls, instance: models.Record, info
        ) -> Union[Type[DiscoRecord], Type[PunkRecord]]:
            # Graphene is unable to accurately determine which type it should resolve without help
            # because the unioned types are all DjangoObjectTypes for the same Record class.
            if instance.is_disco:
                return DiscoRecord
            elif instance.is_punk:
                return PunkRecord
            raise ValueError("Unknown record type")

    Because we have the resolve_type @classmethod defined in our Union, Graphene can correctly determine the record type. Without that we'd get an error any time we tried to resolve values that only exist on the PunkRecord or DiscoRecord type.

    So if we had a records query that returned our Record Union, we could query it as follows without any issues.

    query {
        records {
            ... on DiscoRecord {
                bpm
            }
            ... on PunkRecord {
                maxChords
            }
        }
    }

    But what about the Relay node query? The query looks quite similar to our records query.

    query {
        node(id: "fuga") {
            ... on DiscoRecord {
                bpm
            }
            ... on PunkRecord {
                maxChords
            }
        }
    }

    However, and this is the key difference, node does not return our Union type, but rather our individual DiscoRecord / PunkRecord type. And since both of those types are technically Record types (because of the same Django meta class), any PunkRecords will be resolved asΒ  DiscoRecords and return an error when we try to resolve Punk only fields.

    In order for node to be able to differentiate between the Punk and Disco at the type level we need one more is_type_of classmethod defined on our types.

    class DiscoRecord(graphene_django.DjangoObjectType):
        ...
        @classmethod
        def is_type_of(cls, root, info) -> bool:
            # When a DiscoRecord is resolved as a node it does not use the Union type
            # determine the object's type.
            # Only allow this type to be used with Disco records.
            if isinstance(root, models.Record):
                return root.is_disco
            return False
    
    class PunkRecord(graphene_django.DjangoObjectType):
        ...
        @classmethod
        def is_type_of(cls, root, info) -> bool:
            # When a PunkRecord is resolved as a node it does not use the Union type
            # to determine the object's type.
            # Only allow this type to be used with Punk records.
            if isinstance(root, models.Record):
                return root.is_punk
            return False

    This way, when Graphene is looping through all of our types trying to determine which type to use for a given Record, we can inspect the actual record and prevent an erroneous match.

    This is obvious in retrospect. Although our GraphQL query selectors are exactly the same the root type is different and as such requires a bit more instruction to resolve the appropriate type.
  • TIL: How to change the Docker ENTRYPOINT with Packer

    I've been working on automating setup and deployment for Tanzawa. This necessitates setting up a python 3 with all of the requisite dependencies and then starting a webserver.

    Initially I tried to set the run_command, but that's executed when you build the image, not when you run the image. The command used when running the image is controlled by the ENTRYPOINT, which is Docker specific.

    You can change your ENTRYPOINT by adding it to the "changes" section of your Docker configuration in your packer .pkr.hcl configuration file.

    source "docker" "ubuntu" {
      image  = "python:3"
      commit = true
      changes = [
        "ENTRYPOINT [\"uwsgi --emperor /etc/uwsgi/vassals --uid www-data --gid www-data\"]"
      ]
    }
  • indieweb-utils 0.2.0 was just released. I've been having fun collaborating with James on this project. Really looking forward to dogfooding it by integrating it into Tanzawa.

    Update: Read more about it in James' release blog post.
  • Made my first small PRs to indieweb-utils to pin requirements and introduce pytest. There's a few more I'd like to do e.g. black / flake8 / mypy, but all in due time.
  • Bookmark of Can Matt Mullenweg save the internet?

    He's turning Automattic into a different kind of tech giant. But can he take on the trillion-dollar walled gardens and give the internet back to the people?
    While I agree with Matt that decentralization and individual ownership are central to a Web3, the crypto/blockchain aspect of it is a technological farce.

    Following the principles of IndieWeb on your own domain will allow you, today, to own all of your data and to interact with other people absent of any intermediary service and without melting the arctic.

    A major motivator for building Tanzawa was individual ownership. It's not enough to have your data, but have it stuck in a in serialzied blob in a Wordpress plugin data column somewhere. It's too difficult and cumbersome to reuse. It must be in a proper relational schema. So far the fruits of my indieweb journey have allowed me to not only own my data, but to actually use it toΒ  build upon it. Both trips and maps wouldn't have been possible without Tanzawa.
  • Response to Announcing indieweb-utils

    After some thought, I decided to build indieweb-utils, a Python library with building blocks that will assist developers in building IndieWeb applications.
    indieweb-utils looks like a lovely library to help with some of the faff of html parsing for the IndieWeb.

    I originally planned to do something similar using Tanzawa Indieweb module for Django-Indieweb stuff, but now I'm less convinced that'd be useful outside of the Tanzawa context.Β 

    I'd love to see the Python/Indieweb "consolidate" a bit on a single library so we aren't duplicating effort. I'll have to open some PRs. Great work, James!
  • How to Split Commits

    Sometimes in a rush developing, I'll commit two distinct changes in a single commit. From a code perspective, this isn't an issue because the code works. But from a systems perspective you can no longer split changes from A and B. They're forever married.Β 

    Splitting those changes into two commits will allow us to keep a better history of the system and allow our pull request to "tell a better story".

    We can fix combined commits with an interactive rebase. I use PyCharm for part of this in my regular workflow at work, so rather than providing a concrete example, I'll instead summarize the procedure.

    • git rebase -i origin/mainΒ  (or whatever branch you rebase on to) to start an interactive rebase.
    • Find the commit you want to split and mark it as "edit"
    • git reset HEAD~1
    • Add the files / changes for change A, commit
    • Add the files / changes for change B, commit
    • git rebase --continue

    The "secret" is that when you edit stops the rebase after the combined commit. By resetting HEAD~1, we effectively undo that commit. But since it's a soft reset, the changes are not rolled back, just the commit. This allows us to tweak and commit individual parts separately as desired before continuing to the next commit in our branch.
  • Response to GNUstep: Open-source, Object-oriented, Cross-platform Development Environment

    GNUstep is a mature Framework, suited both for advanced GUI desktop applications as well as server applications. The framework closely follows Apple's Cocoa APIs and is portable to a variety of platforms and architectures.
    Reading this comment really brought back memories of being an Objective-C developer in the early MacOS X days. One thing I lamented in those days was that whatever I wrote was stuck on the Mac and GNUstep gave me hope that it didn't need to be.

    High school me used to think how cool Objective-C and Cocoa was and how it was the future. And thanks to the iPhone, for a long time I was right.

    But the web won the war for Cross-platform development and most days I'm glad it did.
  • I’ve been starting on a refactoring of Tanzawa to help improve maintainability.

    I’m taking a layered approach where each package is broken down into a data layer (models) at the bottom, queries (data access) above that,Β  application (business logic) above that and finally your views at the top.

    The idea being that the top layers can go down the stack, but upper layers can’t go up. I’m not sure if I’m going to enforce it via linting, but I probably will, eventually.

    We’ve been using a similar structure at work and once you get used to it, it’s quick to find the code you’re looking for and keeps things tidy. And linting helps enforce it when we forget or want to be lazy. πŸ˜€
  • Plugins can now be enabled and disabled in production (when running in gunicorn/uWSGI) without bringing the server down πŸ˜€.

    The issue was that only a single process (the one that handled the request) got the plugin dynamically enabled. When the other processes tried to lookup urls/templates from the plugin, it didn't exist as it's not enabled and returned an error.
1 of 10 Next