{"id":22592127,"url":"https://github.com/maroux/python-spanner-orm","last_synced_at":"2025-04-10T23:23:43.970Z","repository":{"id":54687986,"uuid":"263425418","full_name":"maroux/python-spanner-orm","owner":"maroux","description":"Spanner ORM written in Python","archived":false,"fork":false,"pushed_at":"2021-02-03T22:26:00.000Z","size":360,"stargazers_count":2,"open_issues_count":6,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-24T20:11:26.602Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maroux.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-12T18:59:10.000Z","updated_at":"2021-01-30T03:03:32.000Z","dependencies_parsed_at":"2022-08-14T00:01:03.718Z","dependency_job_id":null,"html_url":"https://github.com/maroux/python-spanner-orm","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maroux%2Fpython-spanner-orm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maroux%2Fpython-spanner-orm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maroux%2Fpython-spanner-orm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maroux%2Fpython-spanner-orm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maroux","download_url":"https://codeload.github.com/maroux/python-spanner-orm/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247878022,"owners_count":21011158,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-08T09:15:29.882Z","updated_at":"2025-04-10T23:23:43.940Z","avatar_url":"https://github.com/maroux.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Google Cloud Spanner ORM\n\nThis is a lightweight ORM written in Python and built on top of Cloud Spanner.\n\n## Getting started\n\n### How to install\n\nMake sure that Python 3.7 is the default version of python for your environment,\nthen run:\n`pip install spanner-orm`\n\n### Connecting\n\nTo connect the Spanner ORM to an existing Spanner database:\n\n```python\nimport spanner_orm\nspanner_orm.connect(instance_name, database_name)\n```\n\n`project` and `credentials` are optional parameters, and the standard Spanner\nclient library will attempt to infer them automatically if not specified.\nA session pool may be also specified by the `pool` parameter if necessary. An\nexplanation of session pools may be found\n[here](https://googleapis.github.io/google-cloud-python/latest/spanner/advanced-session-pool-topics.html),\nbut the implementation of TransactionPingingPool in the standard Spanner client\nlibraries seems to not work, and the thread code associated with using the PingingPool\nalso seems to not do what is intended (ping the pool every so often)\n\n### Creating a model\n\nIn order to write to and read from a table on Spanner, you need to tell the ORM\nabout the table by writing a model class, which looks something like this:\n\n```python\nimport spanner_orm\n\nclass TestModel(spanner_orm.Model):\n  __table__ = 'TestTable'  # Name of table in Spanner\n  __interleaved__ = None  # Name of table that the current table is interleaved\n                          # into. None or omitted if the table is not interleaved\n\n  # Every column in the table has a corresponding Field, where the first parameter\n  # is the type of field. The primary key is constructed by the fields labeled\n  # with primary_key=True in the order they appear in the class.\n  # The name of the column is the same as the name of the class attribute\n  id = spanner_orm.Field(spanner_orm.String, primary_key=True)\n  value = spanner_orm.Field(spanner_orm.Integer, nullable=True)\n  number = spanner_orm.Field(spanner_orm.Float, nullable=True)\n\n  # You can specify the size of a string or strings inside an array\n  # Note:\n  # - Size can only be used on `spanner_orm.String` or `spanner_orm.StringArray`\n  # - If size is not specified, it will default to MAX\n  name = spanner_orm.Field(spanner_orm.String, nullable=True, size=10)\n\n  # Secondary indexes are specified in a similar manner to fields:\n  value_index = spanner_orm.Index(['value'])\n\n  # To indicate that there is a foreign key relationship from this table to\n  # another one, use a Relationship.\n  # Note:\n  # - The name of this relationship will be used to name the constraint when\n  #   the DDL is generated for the table\n  # - Spanner supports multi-column foreign key relationships\n  #   To use this, simply add another column relationship in the dictionary\n  fk_table_constraint = spanner_orm.Relationship(\n    'OtherModel',\n    {'value': 'other_model_column'})\n```\n\nIf the model does not refer to an existing table on Spanner, we can create\nthe corresponding table on the database through the ORM in one of two ways. If\nthe database has not yet been created, we can create it and the table at the\nsame time by:\n\n```python\nadmin_api = spanner_orm.connect_admin(\n  'instance_name',\n  'database_name',\n  create_ddl=spanner_orm.model_creation_ddl(TestModel))\nadmin_api.create_database()\n```\n\nIf the database already exists, we can execute a Migration where the upgrade\nmethod returns a CreateTable for the model you have just defined (see section\non migrations)\n\n### Retrieve data from Spanner\n\nAll queries through Spanner take place in a\n[transaction](https://cloud.google.com/spanner/docs/transactions). The ORM\nusually expects a transaction to be present and provided, but if None is\nspecified, a new transaction will be created for that request.\nThe two main ways of retrieving data through the ORM are `where()` and\n`find()`/`find_multi()`:\n\n```python\n# where() is invokes on a model class to retrieve models of that tyep. it takes a\n# transaction and then a sequence of conditions.\n# Most conditions that specify a Field, Index, Relationship, or Model can take\n# either the name of the object or the object itself\ntest_objects = TestModel.where(None, spanner_orm.greater_than('value', '50'))\n\n# To also retrieve related objects, the includes() condition should be used:\ntest_and_other_objects = TestModel.where(None,\n                                         spanner_orm.greater_than(TestModel.value, '50'),\n                                         spanner_orm.includes(TestModel.fake_relationship))\n\n# To create a transaction, run_read_only() or run_write() are used with the\n# method to be run inside the transaction and any arguments to passs to the method.\n# The method is invoked with the transaction as the first argument and then the\n# rest of the provided arguments:\ndef callback_1(transaction, argument):\n  return TestModel.find(transaction, id=argument)\n\nspecific_object = spanner_orm.spanner_api().run_read_only(callback, 1)\n\n# Alternatively, the transactional_read decorator can be used to clean up the\n# call a bit:\n@transactional_read\ndef finder(argument, transaction=None):\n  return TestModel.find(transaction, id=argument)\nspecific_object = finder(1)\n```\n\n### Write data to Spanner\n\nThe simplest way to write data is to create a Model (or retrieve one and modify\nit) and then call save() on it:\n\n```python\ntest_model = TestModel({'key': 'key', 'value': 1})\ntest_model.save()\n```\n\nNote that creating a model as per above will fail if there's already a row in\nthe database where the primary key matches, as it uses a Spanner INSERT instead\nof an UPDATE, as the ORM thinks it's a new object, as it wasn't retrieved from\nSpanner.\n\nFor modifying multiple objects at the same time, the Model `save_batch()` method\ncan be used:\n\n```python\nmodels = []\nfor i in range(10):\n  key = 'test_{}'.format(i)\n  models.append(TestModel({'key': key, 'value': value}))\nTestModel.save_batch(None, models)\n```\n\n`spanner_orm.spanner_api().run_write()` can be used for executing read-write\ntransactions, or the `transactional_write` decorator can be used similarly\nto the read decorator mentioned above. Note that if a transaction fails due to\ndata being modified after the read happened and before the transaction finished\nexecuting, the called method will be re-run until it succeeds or a certain\nnumber of failures happen. Make sure that there are no side effects that could\ncause issues if called multiple times. Exceptions thrown out of the called\nmethod will abort the transaction.\n\nOther helper methods exist for more complex use cases (`create`, `update`,\n`upsert`, and others), but you will have to do more work in order to use those\ncorrectly. See the documentation on those methods for more information.\n\n## Migrations\n\n### Creating migrations\n\nRunning `spanner-orm generate \u003cmigration name\u003e` will generate a new\nmigration file to be filled out in the directory specified (or 'migrations' by\ndefault). The `upgrade` function is executed when migrating, and the\n`downgrade` function is executed when rolling back the migration. Each of\nthese should return a single SchemaUpdate object or a list of SchemaUpdate\nobjects (e.g., CreateTable, AddColumn, etc.).\n\n### Executing migrations\n\nRunning `spanner-orm migrate \u003cSpanner instance\u003e \u003cSpanner database\u003e` will\nexecute all the unmigrated migrations for that database in the correct order,\nusing the application default credentials. If that won't work for your use case,\n`MigrationExecutor` can be used instead:\n\n```python\nconnection = spanner_orm.SpannerConnection(\n  instance_name,\n  database_name,\n  credentials)\nexecutor = spanner_orm.MigrationExecutor(connection)\nexecutor.migrate()\n```\n\n- **Note:** If you need `MigrationExecutor` to import migration files under a\n  particular package name (i.e. not as parent packages) then provide the package\n  name and migration files will be imported using the full module name like so:\n\n  ```python\n  # Migration files will be imported as `project.migrations.migration_name`\n  # Allows you to import other modules from `project` into your migration files\n  executor = spanner_orm.MigrationExecutor(connection, pkg_name=\"project.migrations\")\n  ```\n\nNote that there is no protection against trying execute migrations concurrently\nmultiple times, so try not to do that.\n\nIf a migration needs to be rolled back,\n`spanner-orm rollback \u003cmigration_name\u003e \u003cSpanner instance\u003e \u003cSpanner database\u003e`\nor the corresponding `MigrationExecutor` method should be used.\n\nTo see a list of all migrations found, run `spanner-orm showmigrations \u003cSpanner instance\u003e \u003cSpanner database\u003e`.\nMigrations that have already been applied migrations are marked by an `[X]`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaroux%2Fpython-spanner-orm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaroux%2Fpython-spanner-orm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaroux%2Fpython-spanner-orm/lists"}