<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Using nbtlib
The Named Binary Tag (NBT) file format is a simple structured binary format that is mainly used by the game Minecraft (see the official specification for more details). This short documentation will show you how you can manipulate nbt data using the nbtlib module.
Loading a file
Step1:  By default nbtlib.load will figure out by itself if the specified file is gzipped, but you can also use the gzipped= keyword only argument if you know in advance whether the file is gzipped or not.
Step2:  The nbtlib.load function also accepts the byteorder= keyword only argument. It lets you specify whether the file is big-endian or little-endian. The default value is 'big', which means that the file is interpreted as big-endian by default. You can set it to 'little' to use the little-endian format.
Step3:  Objects returned by the nbtlib.load function are instances of the nbtlib.File class. The nbtlib.load function is actually a small helper around the File.load classmethod. If you need to load files from an already opened file-like object, you can use the File.parse class method.
Step4:  The File class inherits from Compound, which inherits from dict. This means that you can use standard dict operations to access data inside of the file.
Step5:  Modifying files
Step6:  If you don't want to use a context manager, you can call the .save method manually to overwrite the original file or make a copy by specifying a different path. The .save method also accepts the gzipped= keyword only argument. By default, the copy will be gzipped if the original file is gzipped. Similarly, you can use the byteorder= keyword only argument to specify whether the file should be saved using the big-endian or little-endian format. By default, the copy will be saved using the same format as the original file.
Step7:  You can also write nbt data to an already opened file-like object using the .write method.
Step8:  Creating files
Step9:  New files are uncompressed by default. You can use the gzipped= keyword only argument to create a gzipped file. New files are also big-endian by default. You can use the byteorder= keyword only argument to set the endianness of the file to either 'big' or 'little'.
Step10:  Performing operations on tags
With the exception of ByteArray, IntArray and LongArray tags, every tag type inherits from a python builtin, allowing you to make use of their rich and familiar interfaces. ByteArray, IntArray and LongArray tags on the other hand, inherit from numpy arrays instead of the builtin array type in order to benefit from numpy's efficiency.
| Base type           | Associated nbt tags                  |
| ------------------- | ------------------------------------ |
| int             | Byte, Short, Int, Long       |
| float           | Float, Double                    |
| str             | String                             |
| numpy.ndarray   | ByteArray, IntArray, LongArray |
| list            | List                               |
| dict            | Compound                           |
All the methods and operations that are usually available on the the base types can be used on the associated tags.
Step11:  Serializing nbt tags to snbt
While using repr() on nbt tags outputs a python representation of the tag, calling str() on nbt tags (or simply printing them) will return the nbt literal representing that tag.
Step12:  Converting nbt tags to strings will serialize them to snbt. If you want more control over the way nbt tags are serialized, you can use the nbtlib.serialize_tag function. In fact, using str on nbt tags simply calls nbtlib.serialize_tag on the specified tag.
Step13:  You might have noticed that by default, the nbtlib.serialize_tag function will render strings with single ' or double " quotes based on their content to avoid escaping quoting characters. The string is serialized such that the type of quotes used is different from the first quoting character found in the string. If the string doesn't contain any quoting character, the nbtlib.serialize_tag function will render the string as a double " quoted string.
Step14:  You can overwrite this behavior by setting the quote= keyword only argument to either a single ' or a double " quote.
Step15:  The nbtlib.serialize_tag function can be used with the compact= keyword only argument to remove all the extra whitespace from the output.
Step16:  If you'd rather have something a bit more readable, you can use the indent= keyword only argument to tell the nbtlib.serialize_tag function to output indented snbt. The argument can be either a string or an integer and will be used to define how to render each indentation level.
Step17:  If you need the output ot be indented with tabs instead, you can set the indent= argument to '\t'.
Step18:  Note that the indent= keyword only argument can be set to any string, not just '\t'.
Step19:  Creating tags from nbt literals
nbtlib supports creating nbt tags from their literal representation. The nbtlib.parse_nbt function can parse snbt and return the appropriate tag.
Step21:  Note that the parser ignores whitespace.
Step22:  Defining schemas
In order to avoid wrapping values manually every time you edit a compound tag, you can define a schema that will take care of converting python types to predefined nbt tags automatically.
Step23:  By default, you can interact with keys that are not defined in the schema. However, if you use the strict= keyword only argument, the schema instance will raise a TypeError whenever you try to access a key that wasn't defined in the original schema.
Step24:  The schema function is a helper that creates a class that inherits from CompoundSchema. This means that you can also inherit from the class manually.
Step25:  You can also set the strict class attribute to True to create a strict schema type.
Step26:  Combining schemas and custom file types
If you need to deal with files that always have a particular structure, you can create a specialized file type by combining it with a schema. For instance, this is how you would create a file type that opens minecraft structure files.
First, we need to define what a minecraft structure is, so we create a schema that matches the tag hierarchy.
Step27:  Now let's test our schema by creating a structure. We can see that all the types are automatically applied.
Step28:  Now we can create a custom file type that wraps our structure schema. Since structure files are always gzipped we can override the load method to default the gzipped argument to True. We also overwrite the constructor so that it can take directly an instance of our structure schema as argument.
Step29:  We can now use the custom file type to load, edit and save structure files without having to specify the tags manually.
Step30:  So now let's try to edit the structure. We're going to replace all the dirt blocks with stone blocks.
Step31:  As you can see we didn't need to specify any tag to edit the file.
<ASSISTANT_TASK:>
Python Code:
import nbtlib
nbt_file = nbtlib.load('nbt_files/bigtest.nbt')
nbt_file['stringTest']
Explanation: Using nbtlib
The Named Binary Tag (NBT) file format is a simple structured binary format that is mainly used by the game Minecraft (see the official specification for more details). This short documentation will show you how you can manipulate nbt data using the nbtlib module.
Loading a file
End of explanation
uncompressed_file = nbtlib.load('nbt_files/hello_world.nbt', gzipped=False)
uncompressed_file.gzipped
Explanation: By default nbtlib.load will figure out by itself if the specified file is gzipped, but you can also use the gzipped= keyword only argument if you know in advance whether the file is gzipped or not.
End of explanation
little_endian_file = nbtlib.load('nbt_files/hello_world_little.nbt', byteorder='little')
little_endian_file.byteorder
Explanation: The nbtlib.load function also accepts the byteorder= keyword only argument. It lets you specify whether the file is big-endian or little-endian. The default value is 'big', which means that the file is interpreted as big-endian by default. You can set it to 'little' to use the little-endian format.
End of explanation
from nbtlib import File
with open('nbt_files/hello_world.nbt', 'rb') as f:
    hello_world = File.parse(f)
hello_world
Explanation: Objects returned by the nbtlib.load function are instances of the nbtlib.File class. The nbtlib.load function is actually a small helper around the File.load classmethod. If you need to load files from an already opened file-like object, you can use the File.parse class method.
End of explanation
nbt_file.keys()
Explanation: The File class inherits from Compound, which inherits from dict. This means that you can use standard dict operations to access data inside of the file.
End of explanation
from nbtlib.tag import *
with nbtlib.load('nbt_files/demo.nbt') as demo:
    demo['counter'] = Int(demo['counter'] + 1)
demo
Explanation: Modifying files
End of explanation
demo = nbtlib.load('nbt_files/demo.nbt')
...
demo.save()  # overwrite
demo.save('nbt_files/demo_copy.nbt', gzipped=True)  # make a gzipped copy
demo.save('nbt_files/demo_little.nbt', byteorder='little')  # convert the file to little-endian
nbtlib.load('nbt_files/demo_copy.nbt')['counter']
nbtlib.load('nbt_files/demo_little.nbt', byteorder='little')['counter']
Explanation: If you don't want to use a context manager, you can call the .save method manually to overwrite the original file or make a copy by specifying a different path. The .save method also accepts the gzipped= keyword only argument. By default, the copy will be gzipped if the original file is gzipped. Similarly, you can use the byteorder= keyword only argument to specify whether the file should be saved using the big-endian or little-endian format. By default, the copy will be saved using the same format as the original file.
End of explanation
with open('nbt_files/demo_copy.nbt', 'wb') as f:
    demo.write(f)
Explanation: You can also write nbt data to an already opened file-like object using the .write method.
End of explanation
new_file = File({
    'foo': String('bar'),
    'spam': IntArray([1, 2, 3]),
    'egg': List[String](['hello', 'world'])
})
new_file.save('nbt_files/new_file.nbt')
loaded_file = nbtlib.load('nbt_files/new_file.nbt')
loaded_file.gzipped
loaded_file.byteorder
Explanation: Creating files
End of explanation
new_file = File(
    {'thing': LongArray([1, 2, 3])},
    gzipped=True,
    byteorder='little'
)
new_file.save('nbt_files/new_file_gzipped_little.nbt')
loaded_file = nbtlib.load('nbt_files/new_file_gzipped_little.nbt', byteorder='little')
loaded_file.gzipped
loaded_file.byteorder
Explanation: New files are uncompressed by default. You can use the gzipped= keyword only argument to create a gzipped file. New files are also big-endian by default. You can use the byteorder= keyword only argument to set the endianness of the file to either 'big' or 'little'.
End of explanation
my_list = List[String](char.upper() for char in 'hello')
my_list.reverse()
my_list[3:]
my_array = IntArray([1, 2, 3])
my_array + 100
my_pizza = Compound({
    'name': String('Margherita'),
    'price': Double(5.7),
    'size': String('medium')
})
my_pizza.update({'name': String('Calzone'), 'size': String('large')})
my_pizza['price'] = Double(my_pizza['price'] + 2.5)
my_pizza
Explanation: Performing operations on tags
With the exception of ByteArray, IntArray and LongArray tags, every tag type inherits from a python builtin, allowing you to make use of their rich and familiar interfaces. ByteArray, IntArray and LongArray tags on the other hand, inherit from numpy arrays instead of the builtin array type in order to benefit from numpy's efficiency.
| Base type           | Associated nbt tags                  |
| ------------------- | ------------------------------------ |
| int             | Byte, Short, Int, Long       |
| float           | Float, Double                    |
| str             | String                             |
| numpy.ndarray   | ByteArray, IntArray, LongArray |
| list            | List                               |
| dict            | Compound                           |
All the methods and operations that are usually available on the the base types can be used on the associated tags.
End of explanation
example_tag = Compound({
    'numbers': IntArray([1, 2, 3]), 
    'foo': String('bar'),
    'syntax breaking': Float(42),
    'spam': String('{"text":"Hello, world!\\n"}')
})
print(repr(example_tag))
print(str(example_tag))
print(example_tag)
Explanation: Serializing nbt tags to snbt
While using repr() on nbt tags outputs a python representation of the tag, calling str() on nbt tags (or simply printing them) will return the nbt literal representing that tag.
End of explanation
from nbtlib import serialize_tag
print(serialize_tag(example_tag))
serialize_tag(example_tag) == str(example_tag)
Explanation: Converting nbt tags to strings will serialize them to snbt. If you want more control over the way nbt tags are serialized, you can use the nbtlib.serialize_tag function. In fact, using str on nbt tags simply calls nbtlib.serialize_tag on the specified tag.
End of explanation
print(String("contains 'single' quotes"))
print(String('contains "double" quotes'))
print(String('''contains 'single' and "double" quotes'''))
Explanation: You might have noticed that by default, the nbtlib.serialize_tag function will render strings with single ' or double " quotes based on their content to avoid escaping quoting characters. The string is serialized such that the type of quotes used is different from the first quoting character found in the string. If the string doesn't contain any quoting character, the nbtlib.serialize_tag function will render the string as a double " quoted string.
End of explanation
print(serialize_tag(String('forcing "double" quotes'), quote='"'))
Explanation: You can overwrite this behavior by setting the quote= keyword only argument to either a single ' or a double " quote.
End of explanation
print(serialize_tag(example_tag, compact=True))
Explanation: The nbtlib.serialize_tag function can be used with the compact= keyword only argument to remove all the extra whitespace from the output.
End of explanation
nested_tag = Compound({
    'foo': List[Int]([1, 2, 3]),
    'bar': String('name'),
    'values': List[Compound]([
        {'test': String('a'), 'thing': ByteArray([32, 32, 32])},
        {'test': String('b'), 'thing': ByteArray([64, 64, 64])}
    ])
})
print(serialize_tag(nested_tag, indent=4))
Explanation: If you'd rather have something a bit more readable, you can use the indent= keyword only argument to tell the nbtlib.serialize_tag function to output indented snbt. The argument can be either a string or an integer and will be used to define how to render each indentation level.
End of explanation
print(serialize_tag(nested_tag, indent='\t'))
Explanation: If you need the output ot be indented with tabs instead, you can set the indent= argument to '\t'.
End of explanation
print(serialize_tag(nested_tag, indent='.   '))
Explanation: Note that the indent= keyword only argument can be set to any string, not just '\t'.
End of explanation
from nbtlib import parse_nbt
parse_nbt('hello')
parse_nbt('{foo:[{bar:[I;1,2,3]},{spam:6.7f}]}')
Explanation: Creating tags from nbt literals
nbtlib supports creating nbt tags from their literal representation. The nbtlib.parse_nbt function can parse snbt and return the appropriate tag.
End of explanation
parse_nbt({
    foo: [1, 2, 3], 
    bar: "name", 
    values: [
        {
            test: "a", 
            thing: [B; 32B, 32B, 32B]
        }, 
        {
            test: "b", 
            thing: [B; 64B, 64B, 64B]
        }
    ]
})
Explanation: Note that the parser ignores whitespace.
End of explanation
from nbtlib import schema
MySchema = schema('MySchema', {
    'foo': String, 
    'bar': Short
})
my_object = MySchema({'foo': 'hello world', 'bar': 21})
my_object['bar'] *= 2
my_object
Explanation: Defining schemas
In order to avoid wrapping values manually every time you edit a compound tag, you can define a schema that will take care of converting python types to predefined nbt tags automatically.
End of explanation
MyStrictSchema = schema('MyStrictSchema', {
    'foo': String,
    'bar': Short
}, strict=True)
strict_instance = MyStrictSchema()
strict_instance.update({'foo': 'hello world'})
strict_instance
try:
    strict_instance['something'] = List[String](['this', 'raises', 'an', 'error'])
except TypeError as exc:
    print(exc)
Explanation: By default, you can interact with keys that are not defined in the schema. However, if you use the strict= keyword only argument, the schema instance will raise a TypeError whenever you try to access a key that wasn't defined in the original schema.
End of explanation
from nbtlib import CompoundSchema
class MySchema(CompoundSchema):
    schema = {
        'foo': String, 
        'bar': Short
    }
MySchema({'foo': 'hello world', 'bar': 42})
Explanation: The schema function is a helper that creates a class that inherits from CompoundSchema. This means that you can also inherit from the class manually.
End of explanation
class MyStrictSchema(CompoundSchema):
    schema = {
        'foo': String, 
        'bar': Short
    }
    strict = True
try:
    MyStrictSchema({'something': Byte(5)})
except TypeError as exc:
    print(exc)
Explanation: You can also set the strict class attribute to True to create a strict schema type.
End of explanation
Structure = schema('Structure', {
    'DataVersion': Int,
    'author': String,
    'size': List[Int],
    'palette': List[schema('State', {
        'Name': String,
        'Properties': Compound,
    })],
    'blocks': List[schema('Block', {
        'state': Int,
        'pos': List[Int],
        'nbt': Compound,
    })],
    'entities': List[schema('Entity', {
        'pos': List[Double],
        'blockPos': List[Int],
        'nbt': Compound,
    })],
})
Explanation: Combining schemas and custom file types
If you need to deal with files that always have a particular structure, you can create a specialized file type by combining it with a schema. For instance, this is how you would create a file type that opens minecraft structure files.
First, we need to define what a minecraft structure is, so we create a schema that matches the tag hierarchy.
End of explanation
new_structure = Structure({
    'DataVersion': 1139,
    'author': 'dinnerbone',
    'size': [1, 2, 1],
    'palette': [
        {'Name': 'minecraft:dirt'}
    ],
    'blocks': [
        {'pos': [0, 0, 0], 'state': 0},
        {'pos': [0, 1, 0], 'state': 0}
    ],
    'entities': [],
})
type(new_structure['blocks'][0]['pos'])
type(new_structure['entities'])
Explanation: Now let's test our schema by creating a structure. We can see that all the types are automatically applied.
End of explanation
class StructureFile(File, Structure):
    def __init__(self, structure_data=None):
        super().__init__(structure_data or {})
        self.gzipped = True
    @classmethod
    def load(cls, filename, gzipped=True):
        return super().load(filename, gzipped)
Explanation: Now we can create a custom file type that wraps our structure schema. Since structure files are always gzipped we can override the load method to default the gzipped argument to True. We also overwrite the constructor so that it can take directly an instance of our structure schema as argument.
End of explanation
structure_file = StructureFile(new_structure)
structure_file.save('nbt_files/new_structure.nbt')  # you can load it in a minecraft world!
Explanation: We can now use the custom file type to load, edit and save structure files without having to specify the tags manually.
End of explanation
with StructureFile.load('nbt_files/new_structure.nbt') as structure_file:
    structure_file['palette'][0]['Name'] = 'minecraft:stone'
Explanation: So now let's try to edit the structure. We're going to replace all the dirt blocks with stone blocks.
End of explanation
print(serialize_tag(StructureFile.load('nbt_files/new_structure.nbt'), indent=4))
Explanation: As you can see we didn't need to specify any tag to edit the file.
End of explanation
<END_TASK>